Skip to content

Commit 6e979f6

Browse files
committed
feat: introduce "in-memory" behavior
1 parent 9cf50f4 commit 6e979f6

26 files changed

+933
-15
lines changed

config/in_memory.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
4+
5+
use Zenstruck\Foundry\InMemory\InMemoryFactoryRegistry;
6+
use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry;
7+
8+
return static function (ContainerConfigurator $container): void {
9+
$container->services()
10+
->set('.zenstruck_foundry.in_memory.factory_registry', InMemoryFactoryRegistry::class)
11+
->decorate('.zenstruck_foundry.factory_registry')
12+
->arg('$decorated', service('.inner'));
13+
14+
$container->services()
15+
->set('.zenstruck_foundry.in_memory.repository_registry', InMemoryRepositoryRegistry::class)
16+
->arg('$inMemoryRepositories', abstract_arg('inMemoryRepositories'))
17+
;
18+
};

config/services.php

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
service('.zenstruck_foundry.story_registry'),
3434
service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
3535
service('event_dispatcher'),
36+
service('.zenstruck_foundry.in_memory.repository_registry'),
3637
])
3738
->public()
3839
;

src/Configuration.php

+23-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use Zenstruck\Foundry\Exception\FoundryNotBooted;
1818
use Zenstruck\Foundry\Exception\PersistenceDisabled;
1919
use Zenstruck\Foundry\Exception\PersistenceNotAvailable;
20+
use Zenstruck\Foundry\InMemory\CannotEnableInMemory;
21+
use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry;
2022
use Zenstruck\Foundry\Persistence\PersistenceManager;
2123

2224
/**
@@ -43,16 +45,19 @@ final class Configuration
4345
/** @var \Closure():self|self|null */
4446
private static \Closure|self|null $instance = null;
4547

48+
private bool $inMemory = false;
49+
4650
/**
4751
* @phpstan-param InstantiatorCallable $instantiator
4852
*/
4953
public function __construct(
50-
public readonly FactoryRegistry $factories,
54+
public readonly FactoryRegistryInterface $factories,
5155
public readonly Faker\Generator $faker,
5256
callable $instantiator,
5357
public readonly StoryRegistry $stories,
5458
private readonly ?PersistenceManager $persistence = null,
5559
private readonly ?EventDispatcherInterface $eventDispatcher = null,
60+
public readonly ?InMemoryRepositoryRegistry $inMemoryRepositoryRegistry = null,
5661
) {
5762
$this->instantiator = $instantiator;
5863
}
@@ -131,4 +136,21 @@ public static function shutdown(): void
131136
StoryRegistry::reset();
132137
self::$instance = null;
133138
}
139+
140+
/**
141+
* @throws CannotEnableInMemory
142+
*/
143+
public function enableInMemory(): void
144+
{
145+
if (null === $this->inMemoryRepositoryRegistry) {
146+
throw CannotEnableInMemory::noInMemoryRepositoryRegistry();
147+
}
148+
149+
$this->inMemory = true;
150+
}
151+
152+
public function isInMemoryEnabled(): bool
153+
{
154+
return $this->inMemory;
155+
}
134156
}

src/Exception/CannotCreateFactory.php

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the zenstruck/foundry package.
7+
*
8+
* (c) Kevin Bond <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zenstruck\Foundry\Exception;
15+
16+
/**
17+
* @author Nicolas PHILIPPE <[email protected]>
18+
* @internal
19+
*/
20+
final class CannotCreateFactory extends \LogicException
21+
{
22+
public static function argumentCountError(\ArgumentCountError $e): static
23+
{
24+
return new self('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e);
25+
}
26+
}

src/Factory.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Zenstruck\Foundry;
1313

1414
use Faker;
15+
use Zenstruck\Foundry\Exception\CannotCreateFactory;
1516

1617
/**
1718
* @author Kevin Bond <[email protected]>
@@ -53,7 +54,7 @@ final public static function new(array|callable $attributes = []): static
5354
try {
5455
$factory ??= new static(); // @phpstan-ignore new.static
5556
} catch (\ArgumentCountError $e) {
56-
throw new \LogicException('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e);
57+
throw CannotCreateFactory::argumentCountError($e);
5758
}
5859

5960
return $factory

src/FactoryRegistry.php

+9-10
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111

1212
namespace Zenstruck\Foundry;
1313

14+
use Zenstruck\Foundry\Exception\CannotCreateFactory;
15+
1416
/**
1517
* @author Kevin Bond <[email protected]>
1618
*
1719
* @internal
1820
*/
19-
final class FactoryRegistry
21+
final class FactoryRegistry implements FactoryRegistryInterface
2022
{
2123
/**
2224
* @param Factory<mixed>[] $factories
@@ -25,21 +27,18 @@ public function __construct(private iterable $factories)
2527
{
2628
}
2729

28-
/**
29-
* @template T of Factory
30-
*
31-
* @param class-string<T> $class
32-
*
33-
* @return T|null
34-
*/
35-
public function get(string $class): ?Factory
30+
public function get(string $class): Factory
3631
{
3732
foreach ($this->factories as $factory) {
3833
if ($class === $factory::class) {
3934
return $factory; // @phpstan-ignore return.type
4035
}
4136
}
4237

43-
return null;
38+
try {
39+
return new $class();
40+
} catch (\ArgumentCountError $e) {
41+
throw CannotCreateFactory::argumentCountError($e);
42+
}
4443
}
4544
}

src/FactoryRegistryInterface.php

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the zenstruck/foundry package.
5+
*
6+
* (c) Kevin Bond <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Zenstruck\Foundry;
13+
14+
/**
15+
* @author Nicolas PHILIPPE <[email protected]>
16+
*
17+
* @internal
18+
*/
19+
interface FactoryRegistryInterface
20+
{
21+
/**
22+
* @template T of Factory
23+
*
24+
* @param class-string<T> $class
25+
*
26+
* @return T
27+
*/
28+
public function get(string $class): Factory;
29+
}

src/InMemory/AsInMemoryTest.php

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the zenstruck/foundry package.
7+
*
8+
* (c) Kevin Bond <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zenstruck\Foundry\InMemory;
15+
16+
/**
17+
* @author Nicolas PHILIPPE <[email protected]>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
20+
final class AsInMemoryTest
21+
{
22+
/**
23+
* @param class-string $class
24+
* @internal
25+
*/
26+
public static function shouldEnableInMemory(string $class, string $method): bool
27+
{
28+
$classReflection = new \ReflectionClass($class);
29+
30+
if ($classReflection->getAttributes(static::class)) {
31+
return true;
32+
}
33+
34+
return (bool) $classReflection->getMethod($method)->getAttributes(static::class);
35+
}
36+
}

src/InMemory/CannotEnableInMemory.php

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the zenstruck/foundry package.
7+
*
8+
* (c) Kevin Bond <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zenstruck\Foundry\InMemory;
15+
16+
final class CannotEnableInMemory extends \LogicException
17+
{
18+
public static function testIsNotAKernelTestCase(string $testName): self
19+
{
20+
return new self("{$testName}: Cannot use the #[AsInMemoryTest] attribute without extending KernelTestCase.");
21+
}
22+
23+
public static function noInMemoryRepositoryRegistry(): self
24+
{
25+
return new self('Cannot enable "in memory": maybe not in a KernelTestCase?');
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the zenstruck/foundry package.
7+
*
8+
* (c) Kevin Bond <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zenstruck\Foundry\InMemory\DependencyInjection;
15+
16+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17+
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
18+
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\DependencyInjection\Reference;
20+
use Zenstruck\Foundry\InMemory\InMemoryRepository;
21+
22+
/**
23+
* @internal
24+
* @author Nicolas PHILIPPE <[email protected]>
25+
*/
26+
final class InMemoryCompilerPass implements CompilerPassInterface
27+
{
28+
public function process(ContainerBuilder $container): void
29+
{
30+
// create a service locator with all "in memory" repositories, indexed by target class
31+
$inMemoryRepositoriesServices = $container->findTaggedServiceIds('foundry.in_memory.repository');
32+
$inMemoryRepositoriesLocator = ServiceLocatorTagPass::register(
33+
$container,
34+
\array_combine(
35+
\array_map(
36+
static function(string $serviceId) use ($container) {
37+
/** @var class-string<InMemoryRepository<object>> $inMemoryRepositoryClass */
38+
$inMemoryRepositoryClass = $container->getDefinition($serviceId)->getClass() ?? throw new \LogicException("Service \"{$serviceId}\" should have a class.");
39+
40+
return $inMemoryRepositoryClass::_class();
41+
},
42+
\array_keys($inMemoryRepositoriesServices)
43+
),
44+
\array_map(
45+
static fn(string $inMemoryRepositoryId) => new Reference($inMemoryRepositoryId),
46+
\array_keys($inMemoryRepositoriesServices)
47+
),
48+
)
49+
);
50+
51+
$container->findDefinition('.zenstruck_foundry.in_memory.repository_registry')
52+
->setArgument('$inMemoryRepositories', $inMemoryRepositoriesLocator)
53+
;
54+
}
55+
}
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the zenstruck/foundry package.
7+
*
8+
* (c) Kevin Bond <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zenstruck\Foundry\InMemory;
15+
16+
/**
17+
* @template T of object
18+
* @implements InMemoryRepository<T>
19+
* @author Nicolas PHILIPPE <[email protected]>
20+
*
21+
* This class will be used when a specific "in-memory" repository does not exist for a given class.
22+
*/
23+
final class GenericInMemoryRepository implements InMemoryRepository
24+
{
25+
/**
26+
* @var list<T>
27+
*/
28+
private array $elements = [];
29+
30+
/**
31+
* @param class-string<T> $class
32+
*/
33+
public function __construct(
34+
private readonly string $class,
35+
) {
36+
}
37+
38+
/**
39+
* @param T $item
40+
*/
41+
public function _save(object $item): void
42+
{
43+
if (!$item instanceof $this->class) {
44+
throw new \InvalidArgumentException(\sprintf('Given object of class "%s" is not an instance of expected "%s"', $item::class, $this->class));
45+
}
46+
47+
if (!\in_array($item, $this->elements, true)) {
48+
$this->elements[] = $item;
49+
}
50+
}
51+
52+
public function _all(): array
53+
{
54+
return $this->elements;
55+
}
56+
57+
public static function _class(): string
58+
{
59+
throw new \BadMethodCallException('This method should not be called on a GenericInMemoryRepository.');
60+
}
61+
}

0 commit comments

Comments
 (0)