Skip to content

Commit 23b2122

Browse files
committed
feat: introduce "in-memory" behavior
1 parent 1edf948 commit 23b2122

20 files changed

+536
-15
lines changed

composer.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@
4444
},
4545
"autoload": {
4646
"psr-4": { "Zenstruck\\Foundry\\": "src/" },
47-
"files": ["src/functions.php", "src/Persistence/functions.php", "src/phpunit_helper.php"]
47+
"files": [
48+
"src/functions.php",
49+
"src/Persistence/functions.php",
50+
"src/phpunit_helper.php",
51+
"src/InMemory/functions.php"
52+
]
4853
},
4954
"autoload-dev": {
5055
"psr-4": {

config/in_memory.php

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
->arg('$inMemoryRepositoryRegistry', service('.zenstruck_foundry.in_memory.repository_registry'));
14+
15+
$container->services()
16+
->set('.zenstruck_foundry.in_memory.repository_registry', InMemoryRepositoryRegistry::class);
17+
};

src/Configuration.php

+13-1
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ final class Configuration
3636
/** @var \Closure():self|self|null */
3737
private static \Closure|self|null $instance = null;
3838

39+
private bool $inMemory = false;
40+
3941
/**
4042
* @param InstantiatorCallable $instantiator
4143
*/
4244
public function __construct(
43-
public readonly FactoryRegistry $factories,
45+
public readonly FactoryRegistryInterface $factories,
4446
public readonly Faker\Generator $faker,
4547
callable $instantiator,
4648
public readonly StoryRegistry $stories,
@@ -90,4 +92,14 @@ public static function shutdown(): void
9092
StoryRegistry::reset();
9193
self::$instance = null;
9294
}
95+
96+
public function enableInMemory(): void
97+
{
98+
$this->inMemory = true;
99+
}
100+
101+
public function isInMemoryEnabled(): bool
102+
{
103+
return $this->inMemory;
104+
}
93105
}

src/Exception/CannotCreateFactory.php

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Zenstruck\Foundry\Exception;
6+
7+
/**
8+
* @author Nicolas PHILIPPE <[email protected]>
9+
*/
10+
final class CannotCreateFactory extends \LogicException
11+
{
12+
public static function argumentCountError(\ArgumentCountError $e): static
13+
{
14+
return new self('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e);
15+
}
16+
}

src/Factory.php

+2-2
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]>
@@ -33,7 +34,6 @@ public function __construct()
3334
{
3435
}
3536

36-
3737
/**
3838
* @param Attributes $attributes
3939
*/
@@ -46,7 +46,7 @@ final public static function new(array|callable $attributes = []): static
4646
try {
4747
$factory ??= new static(); // @phpstan-ignore-line
4848
} catch (\ArgumentCountError $e) {
49-
throw new \LogicException('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e);
49+
throw CannotCreateFactory::argumentCountError($e);
5050
}
5151

5252
return $factory->initialize()->with($attributes);

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-line
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/AsInMemoryRepository.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Zenstruck\Foundry\InMemory;
6+
7+
// todo: remove this attribute in favor to interface?
8+
#[\Attribute(\Attribute::TARGET_CLASS)]
9+
/**
10+
* @author Nicolas PHILIPPE <[email protected]>
11+
*/
12+
final class AsInMemoryRepository
13+
{
14+
public function __construct(
15+
public readonly string $class
16+
)
17+
{
18+
if (!class_exists($this->class)) {
19+
throw new \InvalidArgumentException("Wrong definition for \"AsInMemoryRepository\" attribute: class \"{$this->class}\" does not exist.");
20+
}
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Zenstruck\Foundry\InMemory\DependencyInjection;
6+
7+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
8+
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
9+
use Symfony\Component\DependencyInjection\ContainerBuilder;
10+
use Symfony\Component\DependencyInjection\Reference;
11+
use Zenstruck\Foundry\InMemory\InMemoryFactoryRegistry;
12+
13+
/**
14+
* @internal
15+
* @author Nicolas PHILIPPE <[email protected]>
16+
*/
17+
final class InMemoryCompilerPass implements CompilerPassInterface
18+
{
19+
public function process(ContainerBuilder $container): void
20+
{
21+
// create a service locator with all "in memory" repositories, indexed by target class
22+
$inMemoryRepositoriesServices = $container->findTaggedServiceIds('foundry.in_memory.repository');
23+
$inMemoryRepositoriesLocator = ServiceLocatorTagPass::register(
24+
$container,
25+
array_combine(
26+
array_map(
27+
static function (array $tags) {
28+
if (\count($tags) !== 1) {
29+
throw new \LogicException('Cannot have multiple tags "foundry.in_memory.repository" on a service!');
30+
}
31+
32+
return $tags[0]['class'] ?? throw new \LogicException('Invalid tag definition of "foundry.in_memory.repository".');
33+
},
34+
array_values($inMemoryRepositoriesServices)
35+
),
36+
array_map(
37+
static fn(string $inMemoryRepositoryId) => new Reference($inMemoryRepositoryId),
38+
array_keys($inMemoryRepositoriesServices)
39+
),
40+
)
41+
);
42+
43+
// todo: should we check we only have a 1 repository per class?
44+
45+
$container->findDefinition('.zenstruck_foundry.in_memory.repository_registry')
46+
->setArgument('$inMemoryRepositories', $inMemoryRepositoriesLocator)
47+
;
48+
}
49+
}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Zenstruck\Foundry\InMemory;
6+
7+
/**
8+
* @template T of object
9+
* @implements InMemoryRepository<T>
10+
* @author Nicolas PHILIPPE <[email protected]>
11+
*
12+
* This class will be used when a specific "in-memory" repository does not exist for a given class.
13+
*/
14+
final class GenericInMemoryRepository implements InMemoryRepository
15+
{
16+
/**
17+
* @var list<T>
18+
*/
19+
private array $elements = [];
20+
21+
/**
22+
* @param class-string<T> $class
23+
*/
24+
public function __construct(
25+
private readonly string $class
26+
)
27+
{
28+
}
29+
30+
/**
31+
* @param T $element
32+
*/
33+
public function _save(object $element): void
34+
{
35+
if (!$element instanceof $this->class) {
36+
throw new \InvalidArgumentException(sprintf('Given object of class "%s" is not an instance of expected "%s"', $element::class, $this->class));
37+
}
38+
39+
if (!in_array($element, $this->elements, true)) {
40+
$this->elements[] = $element;
41+
}
42+
}
43+
}
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Zenstruck\Foundry\InMemory;
6+
7+
use Zenstruck\Foundry\Configuration;
8+
use Zenstruck\Foundry\Factory;
9+
use Zenstruck\Foundry\FactoryRegistryInterface;
10+
use Zenstruck\Foundry\ObjectFactory;
11+
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
12+
13+
/**
14+
* @internal
15+
* @template T of object
16+
* @author Nicolas PHILIPPE <[email protected]>
17+
*/
18+
final class InMemoryFactoryRegistry implements FactoryRegistryInterface
19+
{
20+
public function __construct( // @phpstan-ignore-line
21+
private readonly FactoryRegistryInterface $decorated,
22+
private readonly InMemoryRepositoryRegistry $inMemoryRepositoryRegistry,
23+
) {
24+
}
25+
26+
/**
27+
* @template TFactory of Factory
28+
*
29+
* @param class-string<TFactory> $class
30+
*
31+
* @return TFactory
32+
*/
33+
public function get(string $class): Factory
34+
{
35+
$factory = $this->decorated->get($class);
36+
37+
if (!$factory instanceof ObjectFactory || !Configuration::instance()->isInMemoryEnabled()) {
38+
return $factory;
39+
}
40+
41+
$inMemoryRepository = $this->inMemoryRepositoryRegistry->get($factory::class());
42+
43+
if ($factory instanceof PersistentObjectFactory) {
44+
$factory = $factory->withoutPersisting();
45+
}
46+
47+
return $factory
48+
->afterInstantiate(
49+
function (object $object) use ($inMemoryRepository) {
50+
$inMemoryRepository->_save($object);
51+
}
52+
);
53+
}
54+
}

src/InMemory/InMemoryRepository.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Zenstruck\Foundry\InMemory;
6+
7+
/**
8+
* @author Nicolas PHILIPPE <[email protected]>
9+
*
10+
* @template T of object
11+
*/
12+
interface InMemoryRepository
13+
{
14+
/**
15+
* @param T $element
16+
*/
17+
public function _save(object $element): void;
18+
}

0 commit comments

Comments
 (0)