From cc1ba1dda3f4455a86937613062d9cf5ecf8a1a3 Mon Sep 17 00:00:00 2001 From: Florian Dammeyer Date: Wed, 7 Mar 2018 14:55:42 +0100 Subject: [PATCH 1/6] Added tests for the hydration strategies --- tests/DefinitionTest.php | 2 +- tests/HydrationStrategyTest.php | 102 ++++++++++++++++++++++++++ tests/factories/hydrationstrategy.php | 10 +++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 tests/HydrationStrategyTest.php create mode 100644 tests/factories/hydrationstrategy.php diff --git a/tests/DefinitionTest.php b/tests/DefinitionTest.php index 8519371..8739607 100644 --- a/tests/DefinitionTest.php +++ b/tests/DefinitionTest.php @@ -38,7 +38,7 @@ public function testGetDefinitions() { $definitions = static::$fm->getDefinitions(); - $this->assertCount(39, $definitions); + $this->assertCount(40, $definitions); } public function testBasicDefinitionFunctions() diff --git a/tests/HydrationStrategyTest.php b/tests/HydrationStrategyTest.php new file mode 100644 index 0000000..e3ecacd --- /dev/null +++ b/tests/HydrationStrategyTest.php @@ -0,0 +1,102 @@ +prophesize(HydrationStrategyInterface::class); + + static::$fm->setHydrationStrategy('FakerHydrationModel', $strategy->reveal()); + static::$fm->instance('FakerHydrationModel'); + + $strategy->set(Argument::type('FakerHydrationModel'), Argument::type('string'), Argument::any()) + ->shouldBeCalledTimes(3); + } + + public function testPublicSetterHydration() + { + $strategy = new PublicSetterHydrationStrategy(); + + $model = new ModelWithPublicSetters(); + + $strategy->set($model, 'value', 'Test value'); + $strategy->set($model, 'separated_value', 'Another test value'); + + $this->assertEquals($model->getValue(), 'Test value'); + $this->assertEquals($model->getSeparatedValue(), 'Another test value'); + } + + public function testPublicAttributesHydration() + { + $strategy = new PublicSetterHydrationStrategy(); + + $model = new ModelWithPublicAttributes(); + + $strategy->set($model, 'value', 'Test value'); + $strategy->set($model, 'separated_value', 'Another test value'); + + $this->assertEquals($model->value, 'Test value'); + $this->assertEquals($model->separated_value, 'Another test value'); + } + + public function testProtectedAttributesHydrationByReflection() + { + $strategy = new ReflectionHydrationStrategy(); + + $model = new ModelWithProtectedAttributes(); + + $strategy->set($model, 'value', 'Test value'); + $strategy->set($model, 'separated_value', 'Another test value'); + + $this->assertEquals($model->getValue(), 'Test value'); + $this->assertEquals($model->getSeparatedValue(), 'Another test value'); + } +} + +class ModelWithPublicAttributes +{ + public $value; + public $separated_value; +} + +class ModelWithProtectedAttributes +{ + protected $value; + protected $separated_value; + + public function getValue() + { + return $this->value; + } + + public function getSeparatedValue() + { + return $this->separated_value; + } + +} + +class ModelWithPublicSetters extends ModelWithProtectedAttributes +{ + public function setValue($value) + { + $this->value = $value; + } + + public function setSeparatedValue($separated_value) + { + $this->separated_value = $separated_value; + } +} + +class FakerHydrationModel +{ + public $title; + public $email; + public $text; +} diff --git a/tests/factories/hydrationstrategy.php b/tests/factories/hydrationstrategy.php new file mode 100644 index 0000000..70593e8 --- /dev/null +++ b/tests/factories/hydrationstrategy.php @@ -0,0 +1,10 @@ +define('FakerHydrationModel')->setDefinitions([ + 'title' => Faker::word(), + 'email' => Faker::email(), + 'text' => Faker::text() +]); \ No newline at end of file From fba6490a5e21ee11418abe2f091c17ac2fe39f6f Mon Sep 17 00:00:00 2001 From: Florian Dammeyer Date: Wed, 7 Mar 2018 14:56:17 +0100 Subject: [PATCH 2/6] Added docblock to the $fm property of AbstractTestCase to make IDEs understand the property's type --- tests/AbstractTestCase.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php index 3089e86..8fd075e 100644 --- a/tests/AbstractTestCase.php +++ b/tests/AbstractTestCase.php @@ -21,6 +21,9 @@ */ abstract class AbstractTestCase extends PHPUnit_Framework_TestCase { + /** + * @var FactoryMuffin + */ protected static $fm; public static function setupBeforeClass() From 83a6e51780672a839edf7fbc380be2cc07b3f5a2 Mon Sep 17 00:00:00 2001 From: Florian Dammeyer Date: Wed, 7 Mar 2018 14:56:34 +0100 Subject: [PATCH 3/6] Implemented hydration strategies --- src/FactoryMuffin.php | 67 +++++++++++++++---- .../HydrationStrategyInterface.php | 21 ++++++ .../PublicSetterHydrationStrategy.php | 50 ++++++++++++++ .../ReflectionHydrationStrategy.php | 24 +++++++ 4 files changed, 150 insertions(+), 12 deletions(-) create mode 100644 src/HydrationStrategies/HydrationStrategyInterface.php create mode 100644 src/HydrationStrategies/PublicSetterHydrationStrategy.php create mode 100644 src/HydrationStrategies/ReflectionHydrationStrategy.php diff --git a/src/FactoryMuffin.php b/src/FactoryMuffin.php index aeed6ad..355dc25 100644 --- a/src/FactoryMuffin.php +++ b/src/FactoryMuffin.php @@ -17,6 +17,8 @@ use League\FactoryMuffin\Exceptions\DirectoryNotFoundException; use League\FactoryMuffin\Exceptions\ModelNotFoundException; use League\FactoryMuffin\Generators\GeneratorFactory; +use League\FactoryMuffin\HydrationStrategies\HydrationStrategyInterface; +use League\FactoryMuffin\HydrationStrategies\PublicSetterHydrationStrategy; use League\FactoryMuffin\Stores\ModelStore; use League\FactoryMuffin\Stores\StoreInterface; use RecursiveDirectoryIterator; @@ -55,17 +57,32 @@ class FactoryMuffin protected $factory; /** - * Create a new factory muffin instance. + * The array of registered hydration strategies. * - * @param \League\FactoryMuffin\Stores\StoreInterface|null $store The store instance. - * @param \League\FactoryMuffin\Generators\GeneratorFactory|null $factory The generator factory instance. + * @var \League\FactoryMuffin\HydrationStrategies\HydrationStrategyInterface[] + */ + private $hydration_strategies = []; + + /** + * The default hydration strategy instance that will be used if no specialized + * hydration strategy has been registered for a model class. * - * @return void + * @var \League\FactoryMuffin\HydrationStrategies\HydrationStrategyInterface */ - public function __construct(StoreInterface $store = null, GeneratorFactory $factory = null) + private $default_hydration_strategy; + + /** + * Create a new factory muffin instance. + * + * @param \League\FactoryMuffin\Stores\StoreInterface|null $store The store instance. + * @param \League\FactoryMuffin\Generators\GeneratorFactory|null $factory The generator factory instance. + * @param \League\FactoryMuffin\HydrationStrategies\HydrationStrategyInterface|null $default_hydration_strategy The default hydration strategy instance. + */ + public function __construct(StoreInterface $store = null, GeneratorFactory $factory = null, HydrationStrategyInterface $default_hydration_strategy = null) { $this->store = $store ?: new ModelStore(); $this->factory = $factory ?: new GeneratorFactory(); + $this->default_hydration_strategy = $default_hydration_strategy ?: new PublicSetterHydrationStrategy(); } /** @@ -236,18 +253,44 @@ public function instance($name, array $attr = []) */ protected function generate($model, array $attr = []) { + // Get the hydration strategy that has been + // registered for the given model class + $hydration_strategy = $this->getHydrationStrategy( get_class($model) ); + foreach ($attr as $key => $kind) { $value = $this->factory->generate($kind, $model, $this); - $setter = 'set'.ucfirst(static::camelize($key)); + $hydration_strategy->set($model, $key, $value); + } + } + + /** + * Register a hydration strategy instance that will be used + * to hydrate all models of the given class. + * + * @param string $name The class name of the model. + * @param HydrationStrategyInterface $strategy + */ + public function setHydrationStrategy($name, HydrationStrategyInterface $strategy) + { + $this->hydration_strategies[$name] = $strategy; + } - // check if there is a setter and use it instead - if (method_exists($model, $setter) && is_callable([$model, $setter])) { - $model->$setter($value); - } else { - $model->$key = $value; - } + /** + * Get the hydration strategy for the given model class. + * + * If no specific hydration strategy has been registered, the default strategy will be returned. + * + * @param string $name + * @return HydrationStrategyInterface + */ + public function getHydrationStrategy($name) + { + if (key_exists($name, $this->hydration_strategies)) { + return $this->hydration_strategies[$name]; } + + return $this->default_hydration_strategy; } /** diff --git a/src/HydrationStrategies/HydrationStrategyInterface.php b/src/HydrationStrategies/HydrationStrategyInterface.php new file mode 100644 index 0000000..6b3b4b7 --- /dev/null +++ b/src/HydrationStrategies/HydrationStrategyInterface.php @@ -0,0 +1,21 @@ + + */ +interface HydrationStrategyInterface +{ + /** + * Set the attribute with the given key on + * the given object to the given value. + * + * @param object $model The model instance to set the attribute on. + * @param string $key The key of the attribute to be set. + * @param mixed $value The new value for the given attribute. + */ + public function set($model, $key, $value); +} diff --git a/src/HydrationStrategies/PublicSetterHydrationStrategy.php b/src/HydrationStrategies/PublicSetterHydrationStrategy.php new file mode 100644 index 0000000..540ba4b --- /dev/null +++ b/src/HydrationStrategies/PublicSetterHydrationStrategy.php @@ -0,0 +1,50 @@ + + */ +class PublicSetterHydrationStrategy implements HydrationStrategyInterface +{ + /** + * Set the given attribute by using a public setter method + * or public property access if possible. + * + * @param object $model + * @param string $key + * @param mixed $value + */ + public function set($model, $key, $value) + { + $setter = 'set' . ucfirst(static::camelize($key)); + + // check if there is a setter and use it instead + if (method_exists($model, $setter) && is_callable([$model, $setter])) { + $model->$setter($value); + } else { + $model->$key = $value; + } + } + + /** + * Camelize string. + * + * Transforms a string to camel case (e.g. first_name -> firstName). + * + * @param string $str String in underscore format. + * @return string + */ + protected static function camelize($str) + { + return preg_replace_callback('/_([a-z0-9])/', function ($c) { + return strtoupper($c[1]); + }, $str); + } +} \ No newline at end of file diff --git a/src/HydrationStrategies/ReflectionHydrationStrategy.php b/src/HydrationStrategies/ReflectionHydrationStrategy.php new file mode 100644 index 0000000..599d240 --- /dev/null +++ b/src/HydrationStrategies/ReflectionHydrationStrategy.php @@ -0,0 +1,24 @@ + + */ +class ReflectionHydrationStrategy implements HydrationStrategyInterface +{ + public function set($model, $key, $value) + { + $property = new ReflectionProperty(get_class($model), $key); + $property->setAccessible(true); + $property->setValue($model, $value); + $property->setAccessible(false); + } +} \ No newline at end of file From 8489af60b35a18fc43e73a171304ef529ddbee54 Mon Sep 17 00:00:00 2001 From: Florian Dammeyer Date: Wed, 7 Mar 2018 14:56:52 +0100 Subject: [PATCH 4/6] Updated readme to include the usage of hydration strategies --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index b858834..765f383 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,16 @@ You can also define multiple different model definitions for your models. You ca We have provided a nifty way for you to do this in your tests. PHPUnit provides a `setupBeforeClass` function. Within that function you can call `$fm->loadFactories(__DIR__ . '/factories');`, and it will include all files in the factories folder. Within those php files, you can put your definitions (all your code that calls the define function). The `loadFactories` function will throw a `League\FactoryMuffin\Exceptions\DirectoryNotFoundException` exception if the directory you're loading is not found. +### Model Hydration + +If your model classes use public setter methods or public properties, FactoryMuffin can automatically hydrate your models using its default hydration strategy `League\FactoryMuffin\HydrationStrategies\PublicSetterHydrationStrategy`. + +However, if your models use completely different ways to access their properties, you can implement your own hydration strategies and register them per model. +You simply need to implement the interface `League\FactoryMuffin\HydrationStrategies\HydrationStrategyInterface` and register an instance of your strategy with the class name of your model `$fm->setHydrationStrategy('Fully\Qualified\ModelName', $my_custom_strategy);`. + +For convenience, FactoryMuffin already ships with an alternative hydration strategy called `League\FactoryMuffin\HydrationStrategies\ReflectionHydrationStrategy`. +As the name suggests, this strategy uses reflection to set the attributes directly, which allows it to set `private/protected` properties. You simply need to register an instance of this strategy with every model that you want to be hydrated by this strategy. + ### Creation/Instantiation Callbacks You may optionally specify a callback to be executed on model creation/instantiation as a third parameter when defining a definition. We will pass your model instance as the first parameter to the closure if you specify one. We additionally pass a boolean as the second parameter that will be `true` if the model is being persisted to the database (the create function was used), and `false` if it's not being persisted (the instance function was used). We're using the `isPendingOrSaved` function under the hood here. Note that if you specify a callback and use the create function, we will try to save your model to the database both before and after we execute the callback. From 16a22260ba24914469da95bc4b4cad90d4424517 Mon Sep 17 00:00:00 2001 From: Florian Dammeyer Date: Wed, 7 Mar 2018 15:54:24 +0100 Subject: [PATCH 5/6] Changed HydrationStrategyTest to be compatible with PHP 5.4 --- tests/HydrationStrategyTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/HydrationStrategyTest.php b/tests/HydrationStrategyTest.php index e3ecacd..2f52f07 100644 --- a/tests/HydrationStrategyTest.php +++ b/tests/HydrationStrategyTest.php @@ -1,6 +1,5 @@ prophesize(HydrationStrategyInterface::class); + $strategy = $this->prophesize('League\FactoryMuffin\HydrationStrategies\HydrationStrategyInterface'); static::$fm->setHydrationStrategy('FakerHydrationModel', $strategy->reveal()); static::$fm->instance('FakerHydrationModel'); From c63147e1825cc47683c04b93d0dab70009b7a10d Mon Sep 17 00:00:00 2001 From: Florian Dammeyer Date: Fri, 9 Mar 2018 10:51:49 +0100 Subject: [PATCH 6/6] Apply fixes from StyleCI --- src/FactoryMuffin.php | 13 +++++++------ .../HydrationStrategyInterface.php | 4 ++-- .../PublicSetterHydrationStrategy.php | 7 ++++--- .../ReflectionHydrationStrategy.php | 2 +- tests/HydrationStrategyTest.php | 1 - tests/factories/hydrationstrategy.php | 6 +++--- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/FactoryMuffin.php b/src/FactoryMuffin.php index 355dc25..29095cc 100644 --- a/src/FactoryMuffin.php +++ b/src/FactoryMuffin.php @@ -74,8 +74,8 @@ class FactoryMuffin /** * Create a new factory muffin instance. * - * @param \League\FactoryMuffin\Stores\StoreInterface|null $store The store instance. - * @param \League\FactoryMuffin\Generators\GeneratorFactory|null $factory The generator factory instance. + * @param \League\FactoryMuffin\Stores\StoreInterface|null $store The store instance. + * @param \League\FactoryMuffin\Generators\GeneratorFactory|null $factory The generator factory instance. * @param \League\FactoryMuffin\HydrationStrategies\HydrationStrategyInterface|null $default_hydration_strategy The default hydration strategy instance. */ public function __construct(StoreInterface $store = null, GeneratorFactory $factory = null, HydrationStrategyInterface $default_hydration_strategy = null) @@ -100,7 +100,7 @@ public function seed($times, $name, array $attr = []) { $seeds = []; - for ($i = 0; $i < $times; ++$i) { + for ($i = 0; $i < $times; $i++) { $seeds[] = $this->create($name, $attr); } @@ -255,7 +255,7 @@ protected function generate($model, array $attr = []) { // Get the hydration strategy that has been // registered for the given model class - $hydration_strategy = $this->getHydrationStrategy( get_class($model) ); + $hydration_strategy = $this->getHydrationStrategy(get_class($model)); foreach ($attr as $key => $kind) { $value = $this->factory->generate($kind, $model, $this); @@ -268,7 +268,7 @@ protected function generate($model, array $attr = []) * Register a hydration strategy instance that will be used * to hydrate all models of the given class. * - * @param string $name The class name of the model. + * @param string $name The class name of the model. * @param HydrationStrategyInterface $strategy */ public function setHydrationStrategy($name, HydrationStrategyInterface $strategy) @@ -282,11 +282,12 @@ public function setHydrationStrategy($name, HydrationStrategyInterface $strategy * If no specific hydration strategy has been registered, the default strategy will be returned. * * @param string $name + * * @return HydrationStrategyInterface */ public function getHydrationStrategy($name) { - if (key_exists($name, $this->hydration_strategies)) { + if (array_key_exists($name, $this->hydration_strategies)) { return $this->hydration_strategies[$name]; } diff --git a/src/HydrationStrategies/HydrationStrategyInterface.php b/src/HydrationStrategies/HydrationStrategyInterface.php index 6b3b4b7..cdd3818 100644 --- a/src/HydrationStrategies/HydrationStrategyInterface.php +++ b/src/HydrationStrategies/HydrationStrategyInterface.php @@ -14,8 +14,8 @@ interface HydrationStrategyInterface * the given object to the given value. * * @param object $model The model instance to set the attribute on. - * @param string $key The key of the attribute to be set. - * @param mixed $value The new value for the given attribute. + * @param string $key The key of the attribute to be set. + * @param mixed $value The new value for the given attribute. */ public function set($model, $key, $value); } diff --git a/src/HydrationStrategies/PublicSetterHydrationStrategy.php b/src/HydrationStrategies/PublicSetterHydrationStrategy.php index 540ba4b..1701b59 100644 --- a/src/HydrationStrategies/PublicSetterHydrationStrategy.php +++ b/src/HydrationStrategies/PublicSetterHydrationStrategy.php @@ -19,11 +19,11 @@ class PublicSetterHydrationStrategy implements HydrationStrategyInterface * * @param object $model * @param string $key - * @param mixed $value + * @param mixed $value */ public function set($model, $key, $value) { - $setter = 'set' . ucfirst(static::camelize($key)); + $setter = 'set'.ucfirst(static::camelize($key)); // check if there is a setter and use it instead if (method_exists($model, $setter) && is_callable([$model, $setter])) { @@ -39,6 +39,7 @@ public function set($model, $key, $value) * Transforms a string to camel case (e.g. first_name -> firstName). * * @param string $str String in underscore format. + * * @return string */ protected static function camelize($str) @@ -47,4 +48,4 @@ protected static function camelize($str) return strtoupper($c[1]); }, $str); } -} \ No newline at end of file +} diff --git a/src/HydrationStrategies/ReflectionHydrationStrategy.php b/src/HydrationStrategies/ReflectionHydrationStrategy.php index 599d240..0d80360 100644 --- a/src/HydrationStrategies/ReflectionHydrationStrategy.php +++ b/src/HydrationStrategies/ReflectionHydrationStrategy.php @@ -21,4 +21,4 @@ public function set($model, $key, $value) $property->setValue($model, $value); $property->setAccessible(false); } -} \ No newline at end of file +} diff --git a/tests/HydrationStrategyTest.php b/tests/HydrationStrategyTest.php index 2f52f07..c025d6f 100644 --- a/tests/HydrationStrategyTest.php +++ b/tests/HydrationStrategyTest.php @@ -77,7 +77,6 @@ public function getSeparatedValue() { return $this->separated_value; } - } class ModelWithPublicSetters extends ModelWithProtectedAttributes diff --git a/tests/factories/hydrationstrategy.php b/tests/factories/hydrationstrategy.php index 70593e8..9193286 100644 --- a/tests/factories/hydrationstrategy.php +++ b/tests/factories/hydrationstrategy.php @@ -2,9 +2,9 @@ use League\FactoryMuffin\Faker\Facade as Faker; -/** @var \League\FactoryMuffin\FactoryMuffin $fm */ +/* @var \League\FactoryMuffin\FactoryMuffin $fm */ $fm->define('FakerHydrationModel')->setDefinitions([ 'title' => Faker::word(), 'email' => Faker::email(), - 'text' => Faker::text() -]); \ No newline at end of file + 'text' => Faker::text(), +]);