Skip to content

Commit 7608b78

Browse files
committed
feat: introduce "in-memory" behavior
1 parent dfeb247 commit 7608b78

30 files changed

+741
-69
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
- {php: 8.3, symfony: '*', database: sqlite, without-dama: 1}
3636
- {php: 8.3, symfony: '*', database: sqlite, without-dama: 1, deps: lowest}
3737
- {php: 8.3, symfony: '*', database: mysql, deps: lowest}
38-
- {php: 8.3, symfony: '*', database: mysql, use-migrate: 1}
38+
- {php: 8.3, symfony: '*', database: pgsql, use-migrate: 1}
3939
- {php: 8.3, symfony: '*', database: mysql|mongo, phpunit: 10}
4040
- {php: 8.3, symfony: '*', database: mysql|mongo, phpunit: 11}
4141
- {php: 8.3, symfony: '*', database: mysql|mongo, use-phpunit-extension: 1, phpunit: 11}

composer.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@
4848
"Zenstruck\\Foundry\\": "src/",
4949
"Zenstruck\\Foundry\\Psalm\\": "utils/psalm"
5050
},
51-
"files": ["src/functions.php", "src/Persistence/functions.php", "src/phpunit_helper.php"]
51+
"files": [
52+
"src/functions.php",
53+
"src/Persistence/functions.php",
54+
"src/phpunit_helper.php"
55+
]
5256
},
5357
"autoload-dev": {
5458
"psr-4": {

config/in_memory.php

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
};

config/services.php

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
service('.zenstruck_foundry.instantiator'),
3333
service('.zenstruck_foundry.story_registry'),
3434
service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
35+
service('.zenstruck_foundry.in_memory.repository_registry'),
3536
])
3637
->public()
3738
;

src/Configuration.php

+25-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
use Zenstruck\Foundry\Exception\FoundryNotBooted;
1616
use Zenstruck\Foundry\Exception\PersistenceDisabled;
1717
use Zenstruck\Foundry\Exception\PersistenceNotAvailable;
18+
use Zenstruck\Foundry\InMemory\CannotEnableInMemory;
19+
use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry;
20+
use Zenstruck\Foundry\Object\Instantiator;
1821
use Zenstruck\Foundry\Persistence\PersistenceManager;
1922

2023
/**
@@ -41,15 +44,18 @@ final class Configuration
4144
/** @var \Closure():self|self|null */
4245
private static \Closure|self|null $instance = null;
4346

47+
private bool $inMemory = false;
48+
4449
/**
4550
* @phpstan-param InstantiatorCallable $instantiator
4651
*/
47-
public function __construct(
48-
public readonly FactoryRegistry $factories,
52+
public function __construct( // @phpstan-ignore missingType.generics
53+
public readonly FactoryRegistryInterface $factories,
4954
public readonly Faker\Generator $faker,
5055
callable $instantiator,
5156
public readonly StoryRegistry $stories,
5257
private readonly ?PersistenceManager $persistence = null,
58+
public readonly ?InMemoryRepositoryRegistry $inMemoryRepositoryRegistry = null,
5359
) {
5460
$this->instantiator = $instantiator;
5561
}
@@ -109,4 +115,21 @@ public static function shutdown(): void
109115
StoryRegistry::reset();
110116
self::$instance = null;
111117
}
118+
119+
/**
120+
* @throws CannotEnableInMemory
121+
*/
122+
public function enableInMemory(): void
123+
{
124+
if (null === $this->inMemoryRepositoryRegistry) {
125+
throw CannotEnableInMemory::noInMemoryRepositoryRegistry();
126+
}
127+
128+
$this->inMemory = true;
129+
}
130+
131+
public function isInMemoryEnabled(): bool
132+
{
133+
return $this->inMemory;
134+
}
112135
}

src/Exception/CannotCreateFactory.php

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

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]>
@@ -47,7 +48,7 @@ final public static function new(array|callable $attributes = []): static
4748
try {
4849
$factory ??= new static(); // @phpstan-ignore new.static
4950
} catch (\ArgumentCountError $e) {
50-
throw new \LogicException('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e);
51+
throw CannotCreateFactory::argumentCountError($e);
5152
}
5253

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

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Zenstruck\Foundry\InMemory;
6+
7+
/**
8+
* @author Nicolas PHILIPPE <[email protected]>
9+
*/
10+
#[\Attribute(\Attribute::TARGET_CLASS)]
11+
final class AsInMemoryRepository
12+
{
13+
public function __construct(
14+
public readonly string $class
15+
)
16+
{
17+
if (!class_exists($this->class)) {
18+
throw new \InvalidArgumentException("Wrong definition for \"#[AsInMemoryRepository]\" attribute: class \"{$this->class}\" does not exist.");
19+
}
20+
}
21+
}

src/InMemory/AsInMemoryTest.php

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Zenstruck\Foundry\InMemory;
6+
7+
/**
8+
* @author Nicolas PHILIPPE <[email protected]>
9+
*/
10+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
11+
final class AsInMemoryTest
12+
{
13+
/**
14+
* @param class-string $class
15+
* @internal
16+
*/
17+
public static function shouldEnableInMemory(string $class, string $method): bool
18+
{
19+
$classReflection = new \ReflectionClass($class);
20+
21+
if ($classReflection->getAttributes(static::class)) {
22+
return true;
23+
}
24+
25+
return (bool)$classReflection->getMethod($method)->getAttributes(static::class);
26+
}
27+
}

src/InMemory/CannotEnableInMemory.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+
final class CannotEnableInMemory extends \LogicException
8+
{
9+
public static function testIsNotAKernelTestCase(string $testName): self
10+
{
11+
return new self("{$testName}: Cannot use the #[AsInMemoryTest] attribute without extending KernelTestCase.");
12+
}
13+
14+
public static function noInMemoryRepositoryRegistry(): self
15+
{
16+
return new self('Cannot enable "in memory": maybe not in a KernelTestCase?');
17+
}
18+
}
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+
}
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
44+
public function _all(): array
45+
{
46+
return $this->elements;
47+
}
48+
}

0 commit comments

Comments
 (0)