Skip to content

Commit 3e88d34

Browse files
committed
feat: introduce "in-memory" behavior
1 parent ec2c895 commit 3e88d34

18 files changed

+406
-13
lines changed

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
},
4444
"autoload": {
4545
"psr-4": { "Zenstruck\\Foundry\\": "src/" },
46-
"files": ["src/functions.php", "src/Persistence/functions.php"]
46+
"files": ["src/functions.php", "src/Persistence/functions.php", "src/InMemory/functions.php"]
4747
},
4848
"autoload-dev": {
4949
"psr-4": {

phpunit.dama.xml.dist

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<ini name="error_reporting" value="-1" />
1313
<server name="KERNEL_CLASS" value="Zenstruck\Foundry\Tests\Fixture\TestKernel" />
1414
<server name="SHELL_VERBOSITY" value="-1" />
15+
<server name="APP_ENV" value="test" force="true" />
1516
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0&amp;quiet[]=indirect&amp;quiet[]=other&amp;ignoreFile=./tests/baseline-ignore"/>
1617
<env name="USE_DAMA_DOCTRINE_TEST_BUNDLE" value="1"/>
1718
</php>

phpunit.xml.dist

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<ini name="error_reporting" value="-1" />
1313
<server name="KERNEL_CLASS" value="Zenstruck\Foundry\Tests\Fixture\TestKernel" />
1414
<server name="SHELL_VERBOSITY" value="-1" />
15+
<server name="APP_ENV" value="test" force="true" />
1516
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0&amp;quiet[]=indirect&amp;quiet[]=other&amp;ignoreFile=./tests/baseline-ignore"/>
1617
</php>
1718

src/Configuration.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ final class Configuration
4040
* @param InstantiatorCallable $instantiator
4141
*/
4242
public function __construct(
43-
public readonly FactoryRegistry $factories,
43+
public readonly FactoryRegistryInterface $factories,
4444
public readonly Faker\Generator $faker,
4545
callable $instantiator,
4646
public readonly StoryRegistry $stories,

src/Factory.php

-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ public function __construct()
3333
{
3434
}
3535

36-
3736
/**
3837
* @param Attributes $attributes
3938
*/

src/FactoryRegistry.php

+3-10
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*
1717
* @internal
1818
*/
19-
final class FactoryRegistry
19+
final class FactoryRegistry implements FactoryRegistryInterface
2020
{
2121
/**
2222
* @param Factory<mixed>[] $factories
@@ -25,21 +25,14 @@ public function __construct(private iterable $factories)
2525
{
2626
}
2727

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
28+
public function get(string $class): Factory
3629
{
3730
foreach ($this->factories as $factory) {
3831
if ($class === $factory::class) {
3932
return $factory; // @phpstan-ignore-line
4033
}
4134
}
4235

43-
return null;
36+
return new $class();
4437
}
4538
}

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

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
final class AsInMemoryRepository
10+
{
11+
public function __construct(
12+
public readonly string $class
13+
)
14+
{
15+
if (!class_exists($this->class)) {
16+
throw new \InvalidArgumentException("Wrong definition for \"AsInMemoryRepository\" attribute: class \"{$this->class}\" does not exist.");
17+
}
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
*/
16+
final class InMemoryCompilerPass implements CompilerPassInterface
17+
{
18+
public function process(ContainerBuilder $container): void
19+
{
20+
// create a service locator with all "in memory" repositories, indexed by target class
21+
$inMemoryRepositoriesServices = $container->findTaggedServiceIds('foundry.in_memory.repository');
22+
$inMemoryRepositoriesLocator = ServiceLocatorTagPass::register(
23+
$container,
24+
array_combine(
25+
array_map(
26+
static function (array $tags) {
27+
if (\count($tags) !== 1) {
28+
throw new \LogicException('Cannot have multiple tags "foundry.in_memory.repository" on a service!');
29+
}
30+
31+
return $tags[0]['class'] ?? throw new \LogicException('Invalid tag definition of "foundry.in_memory.repository".');
32+
},
33+
array_values($inMemoryRepositoriesServices)
34+
),
35+
array_map(
36+
static fn(string $inMemoryRepositoryId) => new Reference($inMemoryRepositoryId),
37+
array_keys($inMemoryRepositoriesServices)
38+
),
39+
)
40+
);
41+
42+
// todo: should we check we only have a 1 repository per class?
43+
44+
$container->register('.zenstruck_foundry.in_memory.factory_registry')
45+
->setClass(InMemoryFactoryRegistry::class)
46+
->setDecoratedService('.zenstruck_foundry.factory_registry')
47+
->setArgument('$decorated', $container->getDefinition('.zenstruck_foundry.factory_registry'))
48+
->setArgument('$inMemoryRepositories', $inMemoryRepositoriesLocator)
49+
;
50+
}
51+
}
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Zenstruck\Foundry\InMemory;
6+
7+
use Symfony\Component\DependencyInjection\ServiceLocator;
8+
use Zenstruck\Foundry\Configuration;
9+
use Zenstruck\Foundry\Factory;
10+
use Zenstruck\Foundry\FactoryRegistryInterface;
11+
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
12+
13+
/**
14+
* @internal
15+
*/
16+
final class InMemoryFactoryRegistry implements FactoryRegistryInterface
17+
{
18+
public function __construct( // @phpstan-ignore-line
19+
private readonly FactoryRegistryInterface $decorated,
20+
/** @var ServiceLocator<InMemoryRepository> */
21+
private readonly ServiceLocator $inMemoryRepositories,
22+
) {
23+
}
24+
25+
public function get(string $class): Factory
26+
{
27+
$factory = $this->decorated->get($class);
28+
29+
if (!is_a($class, PersistentObjectFactory::class, allow_string: true)) {
30+
return $factory;
31+
}
32+
33+
$configuration = Configuration::instance();
34+
35+
if (
36+
!$factory instanceof PersistentObjectFactory
37+
|| !$configuration->isPersistenceAvailable()
38+
|| !$configuration->persistence()->isInMemoryEnabled()
39+
) {
40+
return $factory;
41+
}
42+
43+
$factory = $factory->withoutPersisting();
44+
45+
if ($inMemoryRepository = $this->findInMemoryRepository($class)) {
46+
$factory = $factory->afterInstantiate(
47+
static fn(object $object) => $inMemoryRepository->_save($object)
48+
);
49+
}
50+
51+
return $factory->withoutPersisting();
52+
}
53+
54+
/**
55+
* @template T of object
56+
*
57+
* @param class-string<PersistentObjectFactory<T>> $class
58+
*
59+
* @return InMemoryRepository<T>|null
60+
*/
61+
private function findInMemoryRepository(string $class): InMemoryRepository|null
62+
{
63+
$targetClass = $class::class();
64+
if (!$this->inMemoryRepositories->has($targetClass)) {
65+
// todo: should this behavior be opt-in from bundle's configuration?
66+
// ie: throwing here would warn if a class does not have a "in memory" repository
67+
return null;
68+
}
69+
70+
return $this->inMemoryRepositories->get($targetClass);
71+
}
72+
}

src/InMemory/InMemoryRepository.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\InMemory;
6+
7+
/**
8+
* @template T of object
9+
*/
10+
interface InMemoryRepository
11+
{
12+
/**
13+
* @param T $element
14+
*/
15+
public function _save(object $element): void;
16+
}

src/InMemory/functions.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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\InMemory;
13+
14+
use Zenstruck\Foundry\Configuration;
15+
16+
/**
17+
* Enable "in memory" repositories globally.
18+
*/
19+
function enable_in_memory(): void
20+
{
21+
Configuration::instance()->persistence()->enableInMemory();
22+
}

src/Persistence/PersistenceManager.php

+12
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ final class PersistenceManager
3535

3636
private bool $flush = true;
3737
private bool $persist = true;
38+
private bool $inMemory = false;
3839

3940
/**
4041
* @param PersistenceStrategy[] $strategies
@@ -153,6 +154,17 @@ public function enablePersisting(): void
153154
$this->persist = true;
154155
}
155156

157+
public function isInMemoryEnabled(): bool
158+
{
159+
return $this->inMemory;
160+
}
161+
162+
public function enableInMemory(): void
163+
{
164+
$this->persist = false;
165+
$this->inMemory = true;
166+
}
167+
156168
/**
157169
* @template T of object
158170
*

src/ZenstruckFoundryBundle.php

+19
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@
1212
namespace Zenstruck\Foundry;
1313

1414
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
15+
use Symfony\Component\DependencyInjection\ChildDefinition;
1516
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1617
use Symfony\Component\DependencyInjection\ContainerBuilder;
1718
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
1819
use Symfony\Component\DependencyInjection\Reference;
1920
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
21+
use Zenstruck\Foundry\InMemory\AsInMemoryRepository;
22+
use Zenstruck\Foundry\InMemory\DependencyInjection\InMemoryCompilerPass;
23+
use Zenstruck\Foundry\InMemory\InMemoryRepository;
2024
use Zenstruck\Foundry\Object\Instantiator;
2125
use Zenstruck\Foundry\ORM\AbstractORMPersistenceStrategy;
2226

@@ -211,13 +215,28 @@ public function loadExtension(array $config, ContainerConfigurator $configurator
211215
->replaceArgument(1, $config['mongo'])
212216
;
213217
}
218+
219+
// tag with "foundry.in_memory.repository" all classes using attribute "AsInMemoryRepository"
220+
$container->registerAttributeForAutoconfiguration(
221+
AsInMemoryRepository::class,
222+
static function (ChildDefinition $definition, AsInMemoryRepository $attribute, \ReflectionClass $reflector) { // @phpstan-ignore-line
223+
if (!is_a($reflector->name, InMemoryRepository::class, true)) {
224+
throw new \LogicException(sprintf("Service \"%s\" with attribute \"AsInMemoryRepository\" must implement \"%s\".", $reflector->name, InMemoryRepository::class));
225+
}
226+
227+
$definition->addTag('foundry.in_memory.repository', ['class' => $attribute->class]);
228+
}
229+
);
214230
}
215231

216232
public function build(ContainerBuilder $container): void
217233
{
218234
parent::build($container);
219235

220236
$container->addCompilerPass($this);
237+
238+
// todo: should we find a way to decouple Foundry from its "plugins"?
239+
$container->addCompilerPass(new InMemoryCompilerPass());
221240
}
222241

223242
public function process(ContainerBuilder $container): void
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Zenstruck\Foundry\Tests\Fixture\InMemory;
6+
7+
use Zenstruck\Foundry\InMemory\AsInMemoryRepository;
8+
use Zenstruck\Foundry\InMemory\InMemoryRepository;
9+
use Zenstruck\Foundry\Tests\Fixture\Entity\Address\StandardAddress;
10+
11+
/**
12+
* @implements InMemoryRepository<StandardAddress>
13+
*/
14+
#[AsInMemoryRepository(class: StandardAddress::class)]
15+
final class InMemoryStandardAddressRepository implements InMemoryRepository
16+
{
17+
/**
18+
* @var list<StandardAddress>
19+
*/
20+
private array $elements = [];
21+
22+
public function _save(object $element): void
23+
{
24+
if (!in_array($element, $this->elements, true)) {
25+
$this->elements[] = $element;
26+
}
27+
}
28+
29+
/**
30+
* @return list<StandardAddress>
31+
*/
32+
public function all(): array
33+
{
34+
return $this->elements;
35+
}
36+
}

0 commit comments

Comments
 (0)