diff --git a/composer.json b/composer.json index f0da75e..78109fb 100644 --- a/composer.json +++ b/composer.json @@ -22,10 +22,7 @@ "autoload": { "psr-4": { "Laracasts\\TestDummy\\": "src/" - }, - "files": [ - "src/functions.php" - ] + } }, "autoload-dev": { "classmap": [ diff --git a/readme.md b/readme.md index 1b3af78..7d1e160 100644 --- a/readme.md +++ b/readme.md @@ -318,3 +318,61 @@ Factory::$databaseProvider = new MyCustomBuilder; ``` And that's it! Now, whenever you generate and save an entity, TestDummy will reference your custom implementation. + +#### How can I use the bundle with ex. Doctrine? + +To connect DummyTests with ex. Doctrine you need to create an own Adapter class which extends FactoryMethodEloquentModel +class: + +``` +use Laracasts\TestDummy\PersistableModel\FactoryMethodEloquentModel; +use Doctrine\ORM\EntityManager; + +class DoctrineModelAdapter extends FactoryMethodEloquentModel +{ + + protected $em; + + public function __construct(EntityManager $em) + { + $this->em = $em; + } + + /** + * Persist the entity. + * + * @param Entity $entity + * @return void + */ + public function save($entity) + { + $this->em->persist($entity); + $this->em->flush(); + } +} +``` + +Then you need to specify a factory method in you Entity class (ex Foo): + +``` +class Foo extends Entity +{ + //... + public static function createFoo($name) + { + $foo = new static(); + $foo->setName($name); + + return $foo; + } + + //... +} +``` + +Finally, register the Adapter in DummyTests (might be done in AppKernel or bootstrap class). + +``` +$doctrineModelAdapter = new DoctrineModelAdapter($entityManager); +Factory::$databaseProvider = new Laracasts\TestDummy\Builder($doctrineModelAdapter); +``` \ No newline at end of file diff --git a/spec/Laracasts/TestDummy/BuilderSpec.php b/spec/Laracasts/TestDummy/BuilderSpec.php index c31e276..bf7e986 100644 --- a/spec/Laracasts/TestDummy/BuilderSpec.php +++ b/spec/Laracasts/TestDummy/BuilderSpec.php @@ -2,14 +2,17 @@ namespace spec\Laracasts\TestDummy; +use Illuminate\Database\ConnectionResolver; +use Illuminate\Database\Eloquent\Model; use Laracasts\TestDummy\FixturesFinder; +use Laracasts\TestDummy\PersistableModel\FactoryMethodEloquentModel; use PhpSpec\ObjectBehavior; use Prophecy\Argument; -use Laracasts\TestDummy\IsPersistable; +use Laracasts\TestDummy\PersistableModel\IsPersistable; use Laracasts\TestDummy\Factory; -class BuilderSpec extends ObjectBehavior { - +class BuilderSpec extends ObjectBehavior +{ function let(IsPersistable $model) { $factories = (new Factory(__DIR__.'/helpers'))->factories(); @@ -88,6 +91,27 @@ function it_throws_an_exception_if_the_fixture_name_is_not_recognized() $this->shouldThrow('Laracasts\TestDummy\TestDummyException')->duringAttributesFor('Bar'); } + function it_can_create_entity_using_create_method(IsPersistable $model, ConnectionResolver $resolver) + { + $model = new FactoryMethodEloquentModel(); + $factories = (new Factory(__DIR__.'/helpers', $model))->factories(); + $this->beConstructedWith($model, $factories); + + $overrides = ['name' => 'The Boogaloos']; + + $this->build('Foo', $overrides)->name->shouldReturn($overrides['name']); + } + + function it_should_not_allow_to_set_name_as_not_string(IsPersistable $model) + { + $model = new FactoryMethodEloquentModel(); + $factories = (new Factory(__DIR__.'/helpers', $model))->factories(); + $this->beConstructedWith($model, $factories); + + $overrides = ['name' => ['it is an array']]; + + $this->shouldThrow('\InvalidArgumentException')->duringBuild('Foo', $overrides); + } } class AlbumStub { diff --git a/spec/Laracasts/TestDummy/FactoriesLoaderSpec.php b/spec/Laracasts/TestDummy/FactoriesLoaderSpec.php index ac03dfe..0eefc0d 100644 --- a/spec/Laracasts/TestDummy/FactoriesLoaderSpec.php +++ b/spec/Laracasts/TestDummy/FactoriesLoaderSpec.php @@ -17,7 +17,7 @@ function it_loads_a_directory_of_user_provided_factories() $factories = $this->load(__DIR__.'/helpers'); $factories->shouldBeArray(); - $factories->shouldHaveCount(2); + $factories->shouldHaveCount(3); } public function getMatchers() diff --git a/spec/Laracasts/TestDummy/helpers/all.php b/spec/Laracasts/TestDummy/helpers/all.php index 72a7242..9d9033e 100644 --- a/spec/Laracasts/TestDummy/helpers/all.php +++ b/spec/Laracasts/TestDummy/helpers/all.php @@ -10,3 +10,9 @@ 'name' => $faker->name ]; }); + +$factory('Foo', function ($faker) { + return [ + 'name' => $faker->name + ]; +}); diff --git a/src/Builder.php b/src/Builder.php index d0d8f90..ddb39cd 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -5,6 +5,7 @@ use Illuminate\Support\Collection; use Faker\Factory as Faker; use Closure; +use Laracasts\TestDummy\PersistableModel\IsPersistable; class Builder { @@ -111,7 +112,8 @@ public function build($name, $overrides = []) */ public function create($name, array $overrides = []) { - for ($i = 0; $i < $this->getTimes(); $i++) { + $times = $this->getTimes(); + for ($i = 0; $i < $times; $i++) { $entities[] = $this->persist($name, $overrides); } @@ -134,7 +136,11 @@ protected function persist($name, array $attributes = []) $entity = $this->build($name, $attributes); $this->assignRelationships($entity, $attributes); - $this->model->save($entity); + $savedEntity = $this->model->save($entity); + + if (null !== $savedEntity) { + return $savedEntity; + } return $entity; } @@ -166,7 +172,7 @@ protected function getAttributes($name, array $attributes) */ protected function filterRelationshipAttributes(array $attributes) { - return filter_array_keys($attributes, function ($key) { + return $this->filterArrayKeys($attributes, function ($key) { return ! str_contains($key, '.'); }); } @@ -182,7 +188,6 @@ protected function getFixture($name) { // The user may provide either a class name or a short // name identifier. So we'll track it down here. - foreach ($this->fixtures as $fixture) { if ($fixture->shortName == $name) { return $fixture; @@ -236,20 +241,6 @@ protected function runFaker($attribute) $attribute = $attribute(); } - // It's possible that the called Faker method returned an array. - // If that is the case, we'll implode it for the user. - - if (is_array($attribute)) { - - // If we're dealing with an associative array... - - if (array_values($attribute) !== $attribute) { - return array_map([$this, 'runFaker'], $attribute); - } - - return implode(' ', $attribute); - } - return $attribute; } @@ -260,7 +251,7 @@ protected function runFaker($attribute) */ protected function faker() { - if (! $this->faker) { + if ( ! $this->faker) { $this->faker = Faker::create(); } @@ -331,7 +322,7 @@ protected function fetchRelationId($factoryName, $relationshipName, array $attri */ protected function extractRelationshipAttributes($columnName, array $attributes) { - $attributes = filter_array_keys($attributes, function ($key) use ($columnName) { + $attributes = $this->filterArrayKeys($attributes, function ($key) use ($columnName) { return starts_with($key, $columnName . '.'); }); @@ -344,4 +335,19 @@ protected function extractRelationshipAttributes($columnName, array $attributes) return $extractedAttributes; } + + /** + * Filter an array using keys instead of values. + * + * @param array $array + * @param callable $callback + * @return array + */ + protected function filterArrayKeys(array $array, $callback) + { + $matchedKeys = array_filter(array_keys($array), $callback); + + return array_intersect_key($array, array_flip($matchedKeys)); + } + } diff --git a/src/Definition.php b/src/Definition.php index d55f50e..2f0da19 100644 --- a/src/Definition.php +++ b/src/Definition.php @@ -2,6 +2,8 @@ namespace Laracasts\TestDummy; +use Symfony\Component\Process\Exception\LogicException; + class Definition { @@ -10,21 +12,22 @@ class Definition * * @var string */ - public $name; + protected $name; /** * The abbreviated short-name. * * @var string */ - public $shortName; + protected $shortName; /** * Attributes for the factory. * * @var array */ - public $attributes; + protected $attributes; + /** * Create a new Definition instance. @@ -40,4 +43,14 @@ public function __construct($name, $shortName, $attributes = []) $this->attributes = $attributes; } + public function __set($attribute, $value) + { + //We want to keep the object always in the same state + throw new LogicException('You cannot change any value of this object'); + } + + public function __get($attribute) + { + return $this->{$attribute}; + } } diff --git a/src/FactoriesFinder.php b/src/FactoriesFinder.php index d4f5785..ec7a40c 100644 --- a/src/FactoriesFinder.php +++ b/src/FactoriesFinder.php @@ -4,6 +4,7 @@ use RecursiveDirectoryIterator; use RecursiveIteratorIterator; +use RegexIterator; class FactoriesFinder { @@ -32,15 +33,10 @@ function __construct($basePath) */ public function find() { - $files = []; + $iterator = $this->getDirectoryIterator(); + $iterator = new RegexIterator($iterator, '#^.*\.(php)+$#Di'); - foreach ($this->getDirectoryIterator() as $file) { - if ($this->getExtension($file) !== 'php') continue; - - $files[] = $file->getPathname(); - } - - return $files; + return array_keys(iterator_to_array($iterator)); } /** @@ -55,18 +51,4 @@ private function getDirectoryIterator() return new RecursiveIteratorIterator($directoryIterator); } - - /** - * Get the extension of a file. - * - * @param $file - * @return string|null - */ - private function getExtension($file) - { - $fileInfo = pathinfo($file); - - return isset($fileInfo['extension']) ? $fileInfo['extension'] : null; - } - } diff --git a/src/FactoriesLoader.php b/src/FactoriesLoader.php index 60be6a5..e332f87 100644 --- a/src/FactoriesLoader.php +++ b/src/FactoriesLoader.php @@ -18,7 +18,7 @@ public function load($basePath) $designer = new Designer; $faker = new FakerAdapter; - $factory = function ($name, $shortName, $attributes = []) use ($designer, $faker) { + $factory = function ($name, $shortName, $attributes = []) use ($designer) { return $designer->define($name, $shortName, $attributes); }; diff --git a/src/Factory.php b/src/Factory.php index 6ca8f9b..20c689b 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -2,7 +2,8 @@ namespace Laracasts\TestDummy; -use Laracasts\TestDummy\IsPersistable; +use Laracasts\TestDummy\PersistableModel\IsPersistable; +use Laracasts\TestDummy\PersistableModel\EloquentModel; class Factory { @@ -28,16 +29,19 @@ class Factory */ public static $databaseProvider; + protected $builder; + /** * Create a new factory instance. * * @param string $factoriesPath * @param IsPersistable $databaseProvider */ - public function __construct($factoriesPath = null, IsPersistable $databaseProvider = null) + public function __construct($factoriesPath = null, IsPersistable $databaseProvider = null, Builder $builder = null) { $this->loadFactories($factoriesPath); $this->setDatabaseProvider($databaseProvider); + $this->setBuilder($builder); } /** @@ -114,7 +118,7 @@ public static function times($times) */ public function getBuilder() { - return new Builder($this->databaseProvider(), $this->factories()); + return $this->builder; } /** @@ -145,4 +149,13 @@ private function setDatabaseProvider($provider) } } + protected function setBuilder(Builder $builder = null) + { + if (null == $builder) { + $builder = new Builder($this->databaseProvider(), $this->factories()); + } + + $this->builder = $builder; + } + } diff --git a/src/FakerAdapter.php b/src/FakerAdapter.php index 2e25b76..5907f86 100644 --- a/src/FakerAdapter.php +++ b/src/FakerAdapter.php @@ -35,7 +35,7 @@ public function generator() } /** - * Set the locale of the generator + * Sets the locale of the generator * * @param string $locale */ diff --git a/src/EloquentModel.php b/src/PersistableModel/EloquentModel.php similarity index 92% rename from src/EloquentModel.php rename to src/PersistableModel/EloquentModel.php index fd90c3e..7aadde1 100644 --- a/src/EloquentModel.php +++ b/src/PersistableModel/EloquentModel.php @@ -1,8 +1,9 @@ getSaveMethodName($entity); + + if (null == $saveMethodName) { + return parent::build($type, $attributes); + } + + $methodParameters = (new \ReflectionClass($type))->getMethod($saveMethodName)->getParameters(); + + $parameters = array_map(function ($parameter) use ($attributes) { + if (array_key_exists($parameter->name, $attributes)) { + return $attributes[$parameter->name]; + } else if ($parameter->isDefaultValueAvailable()) { + return $parameter->getDefaultValue(); + } + }, $methodParameters); + + $object = forward_static_call_array([$type, $saveMethodName], $parameters); + + //save additional options if not included in add or write method + $methodParameters = array_combine(array_map(function ($parameter) { + return $parameter->getName(); + }, $methodParameters), $parameters); + + foreach ($attributes as $attributeName => $attributeValue) { + if (!isset($methodParameters[$attributeName])) { + $object->$attributeName = $attributeValue; + } + } + + return $object; + } + + protected function getSaveMethodName($className) + { + $allowedSaveMothodNames = array_map(function ($methodName) use ($className) { + return str_replace([ + '{className}' + ], [ + $className + ], $methodName); + }, self::$allowedSaveMethodNames); + + $methodsList = get_class_methods($className); + foreach ($allowedSaveMothodNames as $methodName) { + if (in_array($methodName, $methodsList)) { + return $methodName; + } + } + + return null; + } +} diff --git a/src/IsPersistable.php b/src/PersistableModel/IsPersistable.php similarity index 92% rename from src/IsPersistable.php rename to src/PersistableModel/IsPersistable.php index 9cec361..458c451 100644 --- a/src/IsPersistable.php +++ b/src/PersistableModel/IsPersistable.php @@ -1,6 +1,6 @@ title); + $this->assertInstanceOf('Post', $attributes); + $this->assertEquals('Post Title', $attributes->title); } /** @test */ @@ -73,7 +73,7 @@ public function it_allows_for_overriding_attributes() { $post = TestDummy::build('Post', ['title' => 'override']); - assertEquals('override', $post->title); + $this->assertEquals('override', $post->title); } /** @test */ @@ -81,7 +81,7 @@ public function it_accepts_a_short_name_identifier_instead_of_the_model_class() { $post = TestDummy::build('scheduled_post'); - assertInstanceOf('Post', $post); + $this->assertInstanceOf('Post', $post); } /** @test */ @@ -89,11 +89,11 @@ public function it_allows_a_closure_to_be_used_for_defining_factories() { $comments = TestDummy::times(2)->create('Comment'); - assertInstanceOf('Comment', $comments[0]); - assertInternalType('string', $comments[0]->body); + $this->assertInstanceOf('Comment', $comments[0]); + $this->assertInternalType('string', $comments[0]->body); // Faker should produce a unique value for each generation. - assertNotEquals($comments[0]->body, $comments[1]->body); + $this->assertNotEquals($comments[0]->body, $comments[1]->body); } /** @test */ @@ -101,8 +101,8 @@ public function it_gets_an_array_only_of_attributes() { $attributes = TestDummy::attributesFor('Post', ['title' => 'override']); - assertInternalType('array', $attributes); - assertEquals('override', $attributes['title']); + $this->assertInternalType('array', $attributes); + $this->assertEquals('override', $attributes['title']); } /** @test */ @@ -110,8 +110,8 @@ public function it_builds_and_persists_attributes() { $post = TestDummy::create('Post'); - assertInstanceOf('Post', $post); - assertNotNull($post->id); + $this->assertInstanceOf('Post', $post); + $this->assertNotNull($post->id); } /** @test */ @@ -119,8 +119,8 @@ public function it_builds_up_relationships_if_specified() { $comment = TestDummy::create('Comment'); - assertInstanceOf('Comment', $comment); - assertInstanceOf('Post', $comment->post); + $this->assertInstanceOf('Comment', $comment); + $this->assertInstanceOf('Post', $comment->post); } /** @test */ @@ -128,8 +128,8 @@ public function it_can_build_and_persist_multiple_times() { $posts = TestDummy::times(3)->create('Post'); - assertInstanceOf('Illuminate\Support\Collection', $posts); - assertCount(3, $posts); + $this->assertInstanceOf('Illuminate\Support\Collection', $posts); + $this->assertCount(3, $posts); } /** @@ -154,7 +154,7 @@ public function it_overrides_relationship_attributes_if_specified() 'post_id.title' => 'override' ]); - assertEquals('override', $comment->post->title); + $this->assertEquals('override', $comment->post->title); } /** @test */ @@ -165,8 +165,8 @@ public function it_overrides_relationship_attributes_separately_for_relationship 'receiver_id.name' => 'Jeffrey', ]); - assertEquals('Adam', $message->sender->name); - assertEquals('Jeffrey', $message->receiver->name); + $this->assertEquals('Adam', $message->sender->name); + $this->assertEquals('Jeffrey', $message->receiver->name); } /** @test */ @@ -178,9 +178,9 @@ public function it_can_override_deeply_nested_relationships() 'post_id.author_id.name' => 'Overridden Author Name', ]); - assertEquals('Overridden Comment Body', $comment->body); - assertEquals('Overridden Post Title', $comment->post->title); - assertEquals('Overridden Author Name', $comment->post->author->name); + $this->assertEquals('Overridden Comment Body', $comment->body); + $this->assertEquals('Overridden Post Title', $comment->post->title); + $this->assertEquals('Overridden Author Name', $comment->post->author->name); } /** @test */ @@ -191,8 +191,8 @@ public function relationship_overrides_are_ignored_if_the_relationship_is_not_ac 'post_id.title' => 'override' ]); - assertNull($comment->post); - assertNull($comment->getAttribute('post_id.title')); + $this->assertNull($comment->post); + $this->assertNull($comment->getAttribute('post_id.title')); } } diff --git a/tests/support/models/Foo.php b/tests/support/models/Foo.php index 6890ae5..8729918 100644 --- a/tests/support/models/Foo.php +++ b/tests/support/models/Foo.php @@ -2,4 +2,25 @@ use \Illuminate\Database\Eloquent\Model; -class Foo extends Model {} \ No newline at end of file +class Foo extends Model +{ + public static function createFoo($name) + { + $foo = new Foo(); + $foo->name = $name; + + return $foo; + } + + /** + * @param string $name + */ + protected function setNameAttribute($name) + { + if (!is_string($name)) { + throw new \InvalidArgumentException('The name should be a string'); + } + + $this->attributes['name'] = $name; + } +} \ No newline at end of file