Skip to content

Commit 558fc5f

Browse files
committed
feat: introduce "in-memory" behavior
1 parent 24562e6 commit 558fc5f

31 files changed

+786
-70
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ jobs:
8686
- php: 8.3
8787
deps: highest
8888
symfony: '*'
89-
database: mysql
89+
database: pgsql
9090
use-dama: 1
9191
use-migrate: 1
9292
use-phpunit-extension: 0

composer.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@
4545
},
4646
"autoload": {
4747
"psr-4": { "Zenstruck\\Foundry\\": "src/" },
48-
"files": ["src/functions.php", "src/Persistence/functions.php", "src/phpunit_helper.php"]
48+
"files": [
49+
"src/functions.php",
50+
"src/Persistence/functions.php",
51+
"src/phpunit_helper.php",
52+
"src/InMemory/functions.php"
53+
]
4954
},
5055
"autoload-dev": {
5156
"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')->nullOnInvalid(),
3536
])
3637
->public()
3738
;

src/Configuration.php

+23-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Zenstruck\Foundry\Exception\FoundryNotBooted;
1616
use Zenstruck\Foundry\Exception\PersistenceDisabled;
1717
use Zenstruck\Foundry\Exception\PersistenceNotAvailable;
18+
use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry;
1819
use Zenstruck\Foundry\Persistence\PersistenceManager;
1920

2021
/**
@@ -40,15 +41,18 @@ final class Configuration
4041

4142
private static ?self $instance = null;
4243

44+
private bool $inMemory = false;
45+
4346
/**
4447
* @param InstantiatorCallable $instantiator
4548
*/
46-
public function __construct(
47-
public readonly FactoryRegistry $factories,
49+
public function __construct( // @phpstan-ignore missingType.generics
50+
public readonly FactoryRegistryInterface $factories,
4851
public readonly Faker\Generator $faker,
4952
callable $instantiator,
5053
public readonly StoryRegistry $stories,
5154
private readonly ?PersistenceManager $persistence = null,
55+
public readonly ?InMemoryRepositoryRegistry $inMemoryRepositoryRegistry = null,
5256
) {
5357
$this->instantiator = $instantiator;
5458
}
@@ -93,17 +97,34 @@ public static function boot(\Closure|self $configuration): void
9397
{
9498
self::$instance = \is_callable($configuration) ? ($configuration)() : $configuration;
9599
self::$instance->bootedForDataProvider = false;
100+
self::$instance->inMemory = false;
96101
}
97102

98103
public static function bootForDataProvider(\Closure|self $configuration): void
99104
{
100105
self::$instance = \is_callable($configuration) ? ($configuration)() : $configuration;
101106
self::$instance->bootedForDataProvider = true;
107+
self::$instance->inMemory = false;
102108
}
103109

104110
public static function shutdown(): void
105111
{
106112
StoryRegistry::reset();
107113
self::$instance = null;
108114
}
115+
116+
public function enableInMemory(): void
117+
{
118+
$this->inMemory = true;
119+
}
120+
121+
public function disableInMemory(): void
122+
{
123+
$this->inMemory = false;
124+
}
125+
126+
public function isInMemoryEnabled(): bool
127+
{
128+
return $this->inMemory;
129+
}
109130
}

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
*
@@ -48,7 +48,7 @@ final public static function new(array|callable $attributes = []): static // @ph
4848
try {
4949
$factory ??= new static(); // @phpstan-ignore-line
5050
} catch (\ArgumentCountError $e) {
51-
throw new \LogicException('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e);
51+
throw CannotCreateFactory::argumentCountError($e);
5252
}
5353

5454
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

+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

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