Skip to content

Commit

Permalink
[feature] add Story "pools"
Browse files Browse the repository at this point in the history
  • Loading branch information
kbond committed Mar 4, 2022
1 parent be6b6c8 commit 17aec97
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 16 deletions.
44 changes: 43 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1656,7 +1656,9 @@ Later, you can access the story's state when creating other fixtures:

.. code-block:: php
PostFactory::createOne(['category' => CategoryStory::load()->get('php')]);
PostFactory::createOne([
'category' => CategoryStory::load()->get('php') // Category Proxy
]);
// or use the magic method (functionally equivalent to above)
PostFactory::createOne(['category' => CategoryStory::php()]);
Expand All @@ -1665,6 +1667,46 @@ Later, you can access the story's state when creating other fixtures:

Story state is cleared after each test (unless it is a :ref:`Global State Story <global-state>`).

Story Pools
^^^^^^^^^^^

Stories can store (as state) *pools* of objects:

.. code-block:: php
// src/Story/CategoryStory.php
namespace App\Story;
use App\Factory\ProvinceFactory;
use Zenstruck\Foundry\Story;
final class ProvinceStory extends Story
{
public function build(): void
{
// add collection to a "pool"
$this->addToPool('be', ProvinceFactory::createMany(5, ['country' => 'BE']));
// equivalent to above
$this->addToPool('be', ProvinceFactory::new(['country' => 'BE'])->many(5));
// add single object to a pool
$this->addToPool('be', ProvinceFactory::createOne(['country' => 'BE']));
// add single object to single pool and make available as "state"
$this->addState('be-1', ProvinceFactory::createOne(['country' => 'BE']), 'be');
}
}
Objects can be fetched from pools in your tests, fixtures or other stories:

.. code-block:: php
ProvinceStory::getRandom('be'); // random Province|Proxy from "be" pool
ProvinceStory::getRandomSet('be', 3); // 3 random Province|Proxy's from "be" pool
ProvinceStory::getRandomRange('be', 1, 4); // between 1 and 4 random Province|Proxy's from "be" pool
Bundle Configuration
--------------------

Expand Down
105 changes: 91 additions & 14 deletions src/Story.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ abstract class Story
/** @var array<string, Proxy> */
private $objects = [];

/** @var array<string, Proxy[]> */
private $pools = [];

final public function __call(string $method, array $arguments)
{
return $this->get($method);
}

public static function __callStatic($name, $arguments)
final public static function __callStatic($name, $arguments)
{
return static::load()->get($name);
}
Expand All @@ -29,6 +32,57 @@ final public static function load(): self
}

/**
* Get a random item from a pool.
*/
final public static function getRandom(string $pool): Proxy
{
return static::getRandomSet($pool, 1)[0];
}

/**
* Get a random set of items from a pool.
*
* @return Proxy[]
*/
final public static function getRandomSet(string $pool, int $number): array
{
if ($number < 0) {
throw new \InvalidArgumentException(\sprintf('$number must be positive (%d given).', $number));
}

return static::getRandomRange($pool, $number, $number);
}

/**
* Get a random range of items from a pool.
*
* @return Proxy[]
*/
final public static function getRandomRange(string $pool, int $min, int $max): array
{
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));
}

$story = static::load();
$values = $story->pools[$pool] ?? [];

\shuffle($values);

if (\count($values) < $max) {
throw new \RuntimeException(\sprintf('At least %d items must be on pool "%s" (%d items found).', $max, $pool, \count($values)));
}

return \array_slice($values, 0, \random_int($min, $max));
}

/**
* @param object|Proxy|Factory $object
*
* @return static
*/
final public function add(string $name, object $object): self
Expand All @@ -49,23 +103,48 @@ final public function get(string $name): Proxy

abstract public function build(): void;

final protected function addState(string $name, object $object): self
/**
* @param object|Proxy|Factory|object[]|Proxy[]|Factory[]|FactoryCollection $objects
*
* @return static
*/
final protected function addToPool(string $pool, $objects): self
{
// ensure factories are persisted
if ($object instanceof Factory) {
$object = $object->create();
if ($objects instanceof FactoryCollection) {
$objects = $objects->create();
}

// ensure objects are proxied
if (!$object instanceof Proxy) {
$object = new Proxy($object);
if (!\is_array($objects)) {
$objects = [$objects];
}

// ensure proxies are persisted
if (!$object->isPersisted()) {
$object->save();
foreach ($objects as $object) {
$this->pools[$pool][] = self::normalizeObject($object);
}

return $this;
}

/**
* @param object|Proxy|Factory $object
*
* @return static
*/
final protected function addState(string $name, object $object, ?string $pool = null): self
{
$proxy = self::normalizeObject($object);

$this->objects[$name] = $proxy;

if ($pool) {
$this->addToPool($pool, $proxy);
}

return $this;
}

private static function normalizeObject(object $object): Proxy
{
// ensure factories are persisted
if ($object instanceof Factory) {
$object = $object->create();
Expand All @@ -81,8 +160,6 @@ final protected function addState(string $name, object $object): self
$object->save();
}

$this->objects[$name] = $object;

return $this;
return $object;
}
}
21 changes: 21 additions & 0 deletions tests/Fixtures/Stories/CategoryPoolStory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Zenstruck\Foundry\Tests\Fixtures\Stories;

use Zenstruck\Foundry\Story;
use Zenstruck\Foundry\Tests\Fixtures\Factories\CategoryFactory;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class CategoryPoolStory extends Story
{
public function build(): void
{
$this->addToPool('pool-name', CategoryFactory::createMany(2));
$this->addToPool('pool-name', CategoryFactory::new()->many(3));
$this->addToPool('pool-name', CategoryFactory::createOne());
$this->addToPool('pool-name', CategoryFactory::new());
$this->addState('state-name', CategoryFactory::new(), 'pool-name');
}
}
83 changes: 82 additions & 1 deletion tests/Functional/StoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
use Zenstruck\Foundry\Tests\Fixtures\Factories\CategoryFactory;
use Zenstruck\Foundry\Tests\Fixtures\Factories\PostFactory;
use Zenstruck\Foundry\Tests\Fixtures\Stories\CategoryPoolStory;
use Zenstruck\Foundry\Tests\Fixtures\Stories\CategoryStory;
use Zenstruck\Foundry\Tests\Fixtures\Stories\PostStory;
use Zenstruck\Foundry\Tests\Fixtures\Stories\ServiceStory;
Expand Down Expand Up @@ -100,6 +102,85 @@ public function calling_add_is_deprecated(): void
{
$this->expectDeprecation('Since zenstruck\foundry 1.17.0: Using Story::add() is deprecated, use Story::addState().');

CategoryStory::load()->add('foo', 'bar');
CategoryFactory::assert()->empty();

CategoryStory::load()->add('foo', CategoryFactory::new());

CategoryFactory::assert()->count(3);
}

/**
* @test
*/
public function can_get_random_object_from_pool(): void
{
$ids = [];

while (5 !== \count(\array_unique($ids))) {
$ids[] = CategoryPoolStory::getRandom('pool-name')->getId();
}

$this->assertCount(5, \array_unique($ids));
}

/**
* @test
*/
public function can_get_random_object_set_from_pool(): void
{
$objects = CategoryPoolStory::getRandomSet('pool-name', 3);

$this->assertCount(3, $objects);
$this->assertCount(3, \array_unique(\array_map(static function($category) { return $category->getId(); }, $objects)));
}

/**
* @test
*/
public function can_get_random_object_range_from_pool(): void
{
$counts = [];

while (4 !== \count(\array_unique($counts))) {
$counts[] = \count(CategoryPoolStory::getRandomRange('pool-name', 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 random_range_min_must_be_positive(): void
{
$this->expectException(\InvalidArgumentException::class);

CategoryPoolStory::getRandomRange('pool-name', -1, 25);
}

/**
* @test
*/
public function random_range_min_must_be_less_than_max(): void
{
$this->expectException(\InvalidArgumentException::class);

CategoryPoolStory::getRandomRange('pool-name', 50, 25);
}

/**
* @test
*/
public function random_range_more_than_available(): void
{
$this->expectException(\RuntimeException::class);

CategoryPoolStory::getRandomRange('pool-name', 0, 100);
}
}

0 comments on commit 17aec97

Please sign in to comment.