diff --git a/src/Builder.php b/src/Builder.php index d0d8f90..f4c1b3d 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -139,6 +139,43 @@ protected function persist($name, array $attributes = []) return $entity; } + /** + * Fetch a random existing entity, or create a new one + * + * @param $name + * @param $overrides + * @param array $existingKeys + * @return mixed + * @throws TestDummyException + */ + protected function random($name, $overrides, array $existingKeys = []) + { + $attributes = $this->getAttributes($name, $overrides); + $class = $this->getFixture($name)->name; + + // We'll pass off the process of creating the entity. + // That way, folks can use different persistence layers. + + return $this->model->random($class, $attributes, $existingKeys); + } + + /** + * Assign relationships to a randomly fetched entity + * @param $name + * @param $attributes + * @param array $existingKeys + * @return mixed + */ + public function exists($name, $attributes, array $existingKeys = []) + { + $entity = $this->random($name, $attributes, $existingKeys); + + $this->assignRelationships($entity, $attributes); + $this->model->save($entity); + + return $entity; + } + /** * Merge the fixture with any potential overrides. * @@ -282,12 +319,14 @@ protected function assignRelationships($entity, $attributes) // to see if there are any defined relationships. If there // are, then we'll need to create those records as well. + $existing = []; + foreach ($modelAttributes as $columnName => $value) { if ($relationship = $this->findRelation($value)) { - $entity[$columnName] = $this->fetchRelationId($relationship, $columnName, $attributes); + // $relationship is now our $matches array from findRelation + $existing[] = $entity[$columnName] = $this->fetchRelationId($relationship, $columnName, $attributes, $existing); } } - return $entity; } @@ -299,8 +338,8 @@ protected function assignRelationships($entity, $attributes) */ protected function findRelation($attribute) { - if (is_string($attribute) && preg_match('/^factory:(.+)$/i', $attribute, $matches)) { - return $matches[1]; + if (is_string($attribute) && (preg_match('/^factory:(.+)$/i', $attribute, $matches) || preg_match('/^model:(.+)$/i', $attribute, $matches))) { + return $matches; } return false; @@ -312,12 +351,26 @@ protected function findRelation($attribute) * @param string $factoryName * @param string $relationshipName * @param array $attributes + * @param array $existingKeys * @return int + * @throws \Exception */ - protected function fetchRelationId($factoryName, $relationshipName, array $attributes) + protected function fetchRelationId($factoryName, $relationshipName, array $attributes, array $existingKeys) { - $attributes = $this->extractRelationshipAttributes($relationshipName, $attributes); - $relationKey = $this->persist($factoryName, $attributes)->getKey(); + // $factoryName is our matches, containing both 'model:factoryName'/'factory:factoryName' and just the factoryName + $type = preg_replace('/' . $factoryName[1] . '/', '', $factoryName[0]); + switch ($type) { + case 'factory:': + $attributes = $this->extractRelationshipAttributes($relationshipName, $attributes); + $relationKey = $this->persist($factoryName[1], $attributes)->getKey(); + break; + case 'model:': + $attributes = $this->extractRelationshipAttributes($relationshipName, $attributes); + $relationKey = $this->exists($factoryName[1], $attributes, $existingKeys)->getKey(); + break; + default: + throw new \Exception('Relation identifier not allowed. Please use model or factory.'); + } return $relationKey; } diff --git a/src/EloquentModel.php b/src/EloquentModel.php index fd90c3e..0a991fe 100644 --- a/src/EloquentModel.php +++ b/src/EloquentModel.php @@ -24,6 +24,30 @@ public function build($type, array $attributes) return $this->fill($type, $attributes); } + + /** + * Get a random entity and fill any override attributes + * @param $type + * @param array $attributes + * @param array $existingKeys + * @return mixed + * @throws TestDummyException + */ + public function random($type, array $attributes, array $existingKeys) + { + if ( ! class_exists($type)) { + throw new TestDummyException("The {$type} model was not found."); + } + + $model = $this->getRandom($type, $existingKeys); + + Eloquent::unguard(); + $model->fill($attributes); + Eloquent::reguard(); + + return $model; + } + /** * Persist the entity. * @@ -64,4 +88,23 @@ private function fill($type, $attributes) return $object; } + /** + * Fetch a random entity from the database which is not already in use + * If none exists, create a new one + * + * @param $type + * @param array $existingKeys + * @return mixed + */ + private function getRandom($type, array $existingKeys) + { + $object = new $type; + $count = $type::count() - count($existingKeys); + if ($count > 0) { + $rand = mt_rand(0,$count-1); + return $object->whereNotIn($object->getKeyName(), $existingKeys)->get()[$rand]; + } + return $object; + } + } diff --git a/src/IsPersistable.php b/src/IsPersistable.php index 9cec361..fb078a3 100644 --- a/src/IsPersistable.php +++ b/src/IsPersistable.php @@ -31,4 +31,6 @@ public function save($entity); */ public function getAttributes($entity); + public function random($type, array $attributes, array $existingKeys); + } diff --git a/tests/FactoryTest.php b/tests/FactoryTest.php index 813cd88..9fa69da 100644 --- a/tests/FactoryTest.php +++ b/tests/FactoryTest.php @@ -8,6 +8,7 @@ class FactoryTest extends PHPUnit_Framework_TestCase public function setUp() { parent::setUp(); + date_default_timezone_set("Europe/Stockholm"); TestDummy::$factoriesPath = __DIR__ . '/support/factories'; @@ -64,8 +65,8 @@ public function it_builds_up_attributes_for_an_entity() { $attributes = TestDummy::build('Post'); - assertInstanceOf('Post', $attributes); - assertEquals('Post Title', $attributes->title); + $this->assertInstanceOf('Post', $attributes); + $this->assertEquals('Post Title', $attributes->title); } /** @test */ @@ -73,7 +74,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 +82,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 +90,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 +102,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 +111,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 +120,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 +129,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 +155,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 +166,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 +179,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,9 +192,107 @@ 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')); } + + /* Tests for using existing data + * @author Leo "Phroggyy" Sjöberg + */ + + /** @test */ + public function it_establishes_relationships_if_specified() + { + $post = TestDummy::create('Post'); + $comment = TestDummy::create('comment_for_existing_post'); + + $this->assertInstanceOf('Comment', $comment); + $this->assertInstanceOf('Post', $comment->post); + $this->assertEquals($post->id, $comment->post->id); + } + + /** @test */ + public function it_overrides_existing_relationship_attributes_if_specified() + { + $post = TestDummy::create('Post'); + $comment = TestDummy::create('comment_for_existing_post', [ + 'post_id.title' => 'override' + ]); + + $this->assertEquals('override', $comment->post->title); + } + + /** @test */ + public function it_overrides_existing_relationship_attributes_separately_for_relationships_that_use_the_same_factory() + { + TestDummy::times(2)->create('Person'); //We create 2 to ensure sender_id != receiver_id, since that will create an error + $message = TestDummy::create('message_between_existing_people', [ + 'sender_id.name' => 'Adam', + 'receiver_id.name' => 'Jeffrey', + ]); + + $this->assertEquals('Adam', $message->sender->name); + $this->assertEquals('Jeffrey', $message->receiver->name); + } + + /** @test */ + public function it_handles_both_creating_new_relations_and_using_existing_ones_in_the_same_factory() + { + TestDummy::create('Person'); + $message = TestDummy::create('message_between_existing_and_new_people', [ + 'sender_id.name' => 'Adam', + 'receiver_id.name' => 'Jeffrey', + ]); + + $this->assertEquals('Adam', $message->sender->name); + $this->assertEquals('Jeffrey', $message->receiver->name); + } + + /** @test */ + public function it_can_override_deeply_nested_existing_relationships() + { + TestDummy::create('Person'); + TestDummy::create('post_by_existing_person'); + $comment = TestDummy::create('comment_for_existing_post_by_existing_person', [ + 'body' => 'Overridden Comment Body', + 'post_id.title' => 'Overridden Post Title', + 'post_id.author_id.name' => 'Overridden 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 */ + public function it_creates_models_if_no_existing_are_found() + { + $comment = TestDummy::create('comment_for_existing_post'); + + $this->assertInstanceOf('Comment', $comment); + $this->assertInstanceOf('Post', $comment->post); + } + + /** @test */ + public function it_can_create_and_persist_multiple_times_with_existing() + { + $posts = TestDummy::times(3)->create('post_by_existing_person'); + + $this->assertInstanceOf('Illuminate\Support\Collection', $posts); + $this->assertCount(3, $posts); + // Since we have no existing person the first time, it will be automatically created, but only once... + $this->assertEquals($posts[0]->author, $posts[1]->author); + } + + /** @test */ + public function it_will_create_a_new_when_two_are_required_but_only_one_exists() + { + TestDummy::create('Person'); //We create 2 to ensure sender_id != receiver_id, since that will create an error + $message = TestDummy::create('message_between_existing_people'); + + $this->assertNotEquals($message->sender, $message->receiver); + } + } function comment() diff --git a/tests/support/factories/factories.php b/tests/support/factories/factories.php index 4618dd5..790fbf7 100644 --- a/tests/support/factories/factories.php +++ b/tests/support/factories/factories.php @@ -9,6 +9,11 @@ 'title' => 'Post Title' ]); +$factory('Post', 'post_by_existing_person', [ + 'author_id' => 'model:Person', + 'title' => 'Post Title' +]); + $factory('Comment', function($faker) { return [ @@ -24,6 +29,16 @@ 'body' => $faker->word ]); +$factory('Comment', 'comment_for_existing_post', [ + 'post_id' => 'model:Post', + 'body' => $faker->word +]); + +$factory('Comment', 'comment_for_existing_post_by_existing_person', [ + 'post_id' => 'model:post_by_existing_person', + 'body' => $faker->word +]); + $factory('Foo', function($faker) { return [ 'name' => $faker->word @@ -36,6 +51,18 @@ 'receiver_id' => 'factory:Person', ]); +$factory('Message', 'message_between_existing_people', [ + 'contents' => $faker->sentence, + 'sender_id' => 'model:Person', + 'receiver_id' => 'model:Person', +]); + +$factory('Message', 'message_between_existing_and_new_people', [ + 'contents' => $faker->sentence, + 'sender_id' => 'factory:Person', + 'receiver_id' => 'model:Person', +]); + $factory('Person', [ 'name' => $faker->name ]);