From 4fe0b7f94d569b6a92110042d29ae9d16e734dcc Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Tue, 16 Jun 2020 10:44:58 -0400 Subject: [PATCH 1/2] [feature] add RepositoryProxy::random() and randomSet($min, ?$max) --- src/RepositoryProxy.php | 43 +++++++++++ tests/Fixtures/Entity/Category.php | 5 ++ tests/Functional/RepositoryProxyTest.php | 97 ++++++++++++++++++++++++ 3 files changed, 145 insertions(+) diff --git a/src/RepositoryProxy.php b/src/RepositoryProxy.php index 82d68bc9f..db8d674ee 100644 --- a/src/RepositoryProxy.php +++ b/src/RepositoryProxy.php @@ -124,6 +124,49 @@ public function truncate(): void $om->createQuery("DELETE {$this->getClassName()} e")->execute(); } + /** + * @return Proxy|object + */ + public function random(): Proxy + { + return $this->randomSet(1)[0]; + } + + /** + * @param int $min The minimum number of objects to return (if max is null, will always return this amount) + * @param int|null $max The max number of objects to return + * + * @return Proxy[]|object[] + * + * @throws \RuntimeException if not enough persisted objects to satisfy the max + * @throws \InvalidArgumentException if min is less than zero + * @throws \InvalidArgumentException if max is less than min + */ + public function randomSet(int $min, ?int $max = null): array + { + if (null === $max) { + $max = $min; + } + + if ($min < 0) { + throw new \InvalidArgumentException(\sprintf('Min must be positive (%d given).', $min)); + } + + if ($max < $min) { + throw new \InvalidArgumentException(\sprintf('Max (%d) cannot be less than min (%d).', $max, $min)); + } + + $all = \array_values($this->findAll()); + + \shuffle($all); + + if (\count($all) < $max) { + throw new \RuntimeException(\sprintf('At least %d "%s" object(s) must have been persisted (%d persisted).', $max, $this->getClassName(), \count($all))); + } + + return \array_slice($all, 0, \random_int($min, $max)); + } + /** * @param object|array|mixed $criteria * diff --git a/tests/Fixtures/Entity/Category.php b/tests/Fixtures/Entity/Category.php index a76da2906..6fdb36659 100644 --- a/tests/Fixtures/Entity/Category.php +++ b/tests/Fixtures/Entity/Category.php @@ -21,6 +21,11 @@ class Category */ private $name; + public function getId() + { + return $this->id; + } + public function getName(): ?string { return $this->name; diff --git a/tests/Functional/RepositoryProxyTest.php b/tests/Functional/RepositoryProxyTest.php index 4ca081bd0..72af0b03f 100644 --- a/tests/Functional/RepositoryProxyTest.php +++ b/tests/Functional/RepositoryProxyTest.php @@ -76,4 +76,101 @@ public function find_can_be_passed_proxy_or_object_or_array(): void $this->assertInstanceOf(Proxy::class, $repository->find($proxy->object())); $this->assertInstanceOf(Proxy::class, $repository->find(['name' => 'foo'])); } + + /** + * @test + */ + public function can_find_random_object(): void + { + CategoryFactory::new()->createMany(5); + + $ids = []; + + while (5 !== \count(\array_unique($ids))) { + $ids[] = repository(Category::class)->random()->getId(); + } + + $this->assertCount(5, \array_unique($ids)); + } + + /** + * @test + */ + public function at_least_one_object_must_exist_to_get_random_object(): void + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage(\sprintf('At least 1 "%s" object(s) must have been persisted (0 persisted).', Category::class)); + + repository(Category::class)->random(); + } + + /** + * @test + */ + public function can_find_random_set_of_objects(): void + { + CategoryFactory::new()->createMany(5); + + $objects = repository(Category::class)->randomSet(3); + + $this->assertCount(3, $objects); + $this->assertCount(3, \array_unique(\array_map(fn($category) => $category->getId(), $objects))); + } + + /** + * @test + */ + public function can_find_random_set_of_objects_with_min_and_max(): void + { + CategoryFactory::new()->createMany(5); + + $counts = []; + + while (4 !== \count(\array_unique($counts))) { + $counts[] = \count(repository(Category::class)->randomSet(0, 3)); + } + + $this->assertCount(4, \array_unique($counts)); + $this->assertContains(0, $counts); + $this->assertContains(1, $counts); + $this->assertContains(2, $counts); + $this->assertContains(3, $counts); + $this->assertNotContains(4, $counts); + $this->assertNotContains(5, $counts); + } + + /** + * @test + */ + public function the_number_of_persisted_objects_must_be_at_least_the_random_set_max(): void + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage(\sprintf('At least 2 "%s" object(s) must have been persisted (1 persisted).', Category::class)); + + CategoryFactory::new()->createMany(1); + + repository(Category::class)->randomSet(2); + } + + /** + * @test + */ + public function random_set_min_cannot_be_less_than_zero(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Min must be positive (-1 given).'); + + repository(Category::class)->randomSet(-1); + } + + /** + * @test + */ + public function random_set_max_cannot_be_less_than_min(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Max (3) cannot be less than min (5).'); + + repository(Category::class)->randomSet(5, 3); + } } From 7e9c47adb270b4dea7950f637d51b5eda72c1db3 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Tue, 16 Jun 2020 11:04:35 -0400 Subject: [PATCH 2/2] [feature] add ModelFactory::random() and randomSet($min, ?$max) --- README.md | 14 +++++++ src/Bundle/Resources/skeleton/Factory.tpl.php | 2 + src/ModelFactory.php | 21 ++++++++++ tests/Functional/ModelFactoryTest.php | 42 +++++++++++++++++++ 4 files changed, 79 insertions(+) diff --git a/README.md b/README.md index d0a118abf..6cef41d1e 100644 --- a/README.md +++ b/README.md @@ -563,6 +563,9 @@ $repository->assertNotExists(['title' => 'My Title']); $repository->getCount(); // number of rows in the database table $repository->first(); // get the first object (wrapped in a object proxy) $repository->truncate(); // delete all rows in the database table +$repository->random(); // get a random object +$repository->randomSet(5); // get 5 random objects +$repository->randomSet(0, 5); // get 0-5 random objects // instance of ObjectRepository (all returned objects are proxied) $repository->find(1); // Proxy|Post|null @@ -603,6 +606,8 @@ use Zenstruck\Foundry\Proxy; /** * @method static Post|Proxy findOrCreate(array $attributes) + * @method static Post|Proxy random() + * @method static Post[]|Proxy[] randomSet(int $min, ?int $max = null) * @method static PostRepository|RepositoryProxy repository(bool $proxy = true) * @method Post instantiate($attributes = []) * @method Post[] instantiateMany(int $number, $attributes = []) @@ -646,6 +651,15 @@ PostFactory::new(['title' => 'My Title'])->create(); // alternative to above // find a persisted object for the given attributes, if not found, create with the attributes PostFactory::findOrCreate(['title' => 'My Title']); // instance of Proxy|Post +// get a random object that has been persisted +PostFactory::random(); // instance of Proxy|Post + +// get a random set of objects that have been persisted +PostFactory::randomSet(4); // array containing 4 instances of Proxy|Post's + +// random set of persisted objects with min/max +PostFactory::randomSet(0, 5); // array containing 0-5 instances of Proxy|Post's + PostFactory::repository(); // Instance of RepositoryProxy|PostRepository // instantiate objects (without persisting) diff --git a/src/Bundle/Resources/skeleton/Factory.tpl.php b/src/Bundle/Resources/skeleton/Factory.tpl.php index d8b4dd99e..fd36f30b1 100644 --- a/src/Bundle/Resources/skeleton/Factory.tpl.php +++ b/src/Bundle/Resources/skeleton/Factory.tpl.php @@ -11,6 +11,8 @@ /** * @method static getShortName() ?>|Proxy findOrCreate(array $attributes) + * @method static getShortName() ?>|Proxy random() + * @method static getShortName() ?>[]|Proxy[] randomSet(int $min, ?int $max = null) * @method static getShortName() ?>|RepositoryProxy repository(bool $proxy = true) * @method getShortName() ?> instantiate($attributes = []) diff --git a/src/ModelFactory.php b/src/ModelFactory.php index c9f2a7213..ac501e886 100644 --- a/src/ModelFactory.php +++ b/src/ModelFactory.php @@ -59,6 +59,27 @@ final public static function findOrCreate(array $attributes): object return self::new()->create($attributes); } + /** + * Get a random persisted object. + * + * @return Proxy|object + */ + final public static function random(): object + { + return self::repository(true)->random(); + } + + /** + * @param int $min The minimum number of objects to return (if max is null, will always return this amount) + * @param int|null $max The max number of objects to return + * + * @return Proxy[]|object[] + */ + final public static function randomSet(int $min, ?int $max = null): array + { + return self::repository(true)->randomSet($min, $max); + } + /** * @return RepositoryProxy|ObjectRepository */ diff --git a/tests/Functional/ModelFactoryTest.php b/tests/Functional/ModelFactoryTest.php index fb8d255ad..a06ff933f 100644 --- a/tests/Functional/ModelFactoryTest.php +++ b/tests/Functional/ModelFactoryTest.php @@ -56,4 +56,46 @@ public function initialize_must_return_a_value(): void PostFactoryWithNullInitialize::new(); } + + public function can_find_random_object(): void + { + CategoryFactory::new()->createMany(5); + + $ids = []; + + while (5 !== \count(\array_unique($ids))) { + $ids[] = CategoryFactory::random()->getId(); + } + + $this->assertCount(5, \array_unique($ids)); + } + + public function can_find_random_set_of_objects(): void + { + CategoryFactory::new()->createMany(5); + + $objects = CategoryFactory::randomSet(3); + + $this->assertCount(3, $objects); + $this->assertCount(3, \array_unique(\array_map(fn($category) => $category->getId(), $objects))); + } + + public function can_find_random_set_of_objects_with_min_and_max(): void + { + CategoryFactory::new()->createMany(5); + + $counts = []; + + while (4 !== \count(\array_unique($counts))) { + $counts[] = \count(CategoryFactory::randomSet(0, 3)); + } + + $this->assertCount(4, \array_unique($counts)); + $this->assertContains(0, $counts); + $this->assertContains(1, $counts); + $this->assertContains(2, $counts); + $this->assertContains(3, $counts); + $this->assertNotContains(4, $counts); + $this->assertNotContains(5, $counts); + } }