Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: "in memory" behavior #590

Draft
wants to merge 1 commit into
base: 2.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- {php: 8.3, symfony: '*', database: sqlite, without-dama: 1}
- {php: 8.3, symfony: '*', database: sqlite, without-dama: 1, deps: lowest}
- {php: 8.3, symfony: '*', database: mysql, deps: lowest}
- {php: 8.3, symfony: '*', database: mysql, use-migrate: 1}
- {php: 8.3, symfony: '*', database: pgsql, use-migrate: 1}
- {php: 8.3, symfony: '*', database: mysql|mongo, phpunit: 10}
- {php: 8.3, symfony: '*', database: mysql|mongo, phpunit: 11}
- {php: 8.3, symfony: '*', database: mysql|mongo, use-phpunit-extension: 1, phpunit: 11}
Expand Down
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@
"Zenstruck\\Foundry\\": "src/",
"Zenstruck\\Foundry\\Psalm\\": "utils/psalm"
},
"files": ["src/functions.php", "src/Persistence/functions.php", "src/phpunit_helper.php"]
"files": [
"src/functions.php",
"src/Persistence/functions.php",
"src/phpunit_helper.php"
]
},
"autoload-dev": {
"psr-4": {
Expand Down
16 changes: 16 additions & 0 deletions config/in_memory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Zenstruck\Foundry\InMemory\InMemoryFactoryRegistry;
use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry;

return static function (ContainerConfigurator $container): void {
$container->services()
->set('.zenstruck_foundry.in_memory.factory_registry', InMemoryFactoryRegistry::class)
->decorate('.zenstruck_foundry.factory_registry')
->arg('$decorated', service('.inner'));

$container->services()
->set('.zenstruck_foundry.in_memory.repository_registry', InMemoryRepositoryRegistry::class);
};
1 change: 1 addition & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
service('.zenstruck_foundry.instantiator'),
service('.zenstruck_foundry.story_registry'),
service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
service('.zenstruck_foundry.in_memory.repository_registry'),
])
->public()
;
Expand Down
27 changes: 25 additions & 2 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
use Zenstruck\Foundry\Exception\FoundryNotBooted;
use Zenstruck\Foundry\Exception\PersistenceDisabled;
use Zenstruck\Foundry\Exception\PersistenceNotAvailable;
use Zenstruck\Foundry\InMemory\CannotEnableInMemory;
use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry;
use Zenstruck\Foundry\Object\Instantiator;
use Zenstruck\Foundry\Persistence\PersistenceManager;

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

private bool $inMemory = false;

/**
* @phpstan-param InstantiatorCallable $instantiator
*/
public function __construct(
public readonly FactoryRegistry $factories,
public function __construct( // @phpstan-ignore missingType.generics
public readonly FactoryRegistryInterface $factories,
public readonly Faker\Generator $faker,
callable $instantiator,
public readonly StoryRegistry $stories,
private readonly ?PersistenceManager $persistence = null,
public readonly ?InMemoryRepositoryRegistry $inMemoryRepositoryRegistry = null,
) {
$this->instantiator = $instantiator;
}
Expand Down Expand Up @@ -109,4 +115,21 @@ public static function shutdown(): void
StoryRegistry::reset();
self::$instance = null;
}

/**
* @throws CannotEnableInMemory
*/
public function enableInMemory(): void
{
if (null === $this->inMemoryRepositoryRegistry) {
throw CannotEnableInMemory::noInMemoryRepositoryRegistry();
}

$this->inMemory = true;
}

public function isInMemoryEnabled(): bool
{
return $this->inMemory;
}
}
17 changes: 17 additions & 0 deletions src/Exception/CannotCreateFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\Exception;

/**
* @author Nicolas PHILIPPE <[email protected]>
* @internal
*/
final class CannotCreateFactory extends \LogicException
{
public static function argumentCountError(\ArgumentCountError $e): static
{
return new self('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e);
}
}
3 changes: 2 additions & 1 deletion src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Zenstruck\Foundry;

use Faker;
use Zenstruck\Foundry\Exception\CannotCreateFactory;

/**
* @author Kevin Bond <[email protected]>
Expand Down Expand Up @@ -47,7 +48,7 @@ final public static function new(array|callable $attributes = []): static
try {
$factory ??= new static(); // @phpstan-ignore new.static
} catch (\ArgumentCountError $e) {
throw new \LogicException('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e);
throw CannotCreateFactory::argumentCountError($e);
}

return $factory->initialize()->with($attributes);
Expand Down
19 changes: 9 additions & 10 deletions src/FactoryRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@

namespace Zenstruck\Foundry;

use Zenstruck\Foundry\Exception\CannotCreateFactory;

/**
* @author Kevin Bond <[email protected]>
*
* @internal
*/
final class FactoryRegistry
final class FactoryRegistry implements FactoryRegistryInterface
{
/**
* @param Factory<mixed>[] $factories
Expand All @@ -25,21 +27,18 @@ public function __construct(private iterable $factories)
{
}

/**
* @template T of Factory
*
* @param class-string<T> $class
*
* @return T|null
*/
public function get(string $class): ?Factory
public function get(string $class): Factory
{
foreach ($this->factories as $factory) {
if ($class === $factory::class) {
return $factory; // @phpstan-ignore return.type
}
}

return null;
try {
return new $class();
} catch (\ArgumentCountError $e) {
throw CannotCreateFactory::argumentCountError($e);
}
}
}
29 changes: 29 additions & 0 deletions src/FactoryRegistryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of the zenstruck/foundry package.
*
* (c) Kevin Bond <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Zenstruck\Foundry;

/**
* @author Nicolas PHILIPPE <[email protected]>
*
* @internal
*/
interface FactoryRegistryInterface
{
/**
* @template T of Factory
*
* @param class-string<T> $class
*
* @return T
*/
public function get(string $class): Factory;
}
21 changes: 21 additions & 0 deletions src/InMemory/AsInMemoryRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\InMemory;

/**
* @author Nicolas PHILIPPE <[email protected]>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class AsInMemoryRepository
{
public function __construct(
public readonly string $class
)
{
if (!class_exists($this->class)) {
throw new \InvalidArgumentException("Wrong definition for \"#[AsInMemoryRepository]\" attribute: class \"{$this->class}\" does not exist.");
}
}
}
27 changes: 27 additions & 0 deletions src/InMemory/AsInMemoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\InMemory;

/**
* @author Nicolas PHILIPPE <[email protected]>
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
final class AsInMemoryTest
{
/**
* @param class-string $class
* @internal
*/
public static function shouldEnableInMemory(string $class, string $method): bool
{
$classReflection = new \ReflectionClass($class);

if ($classReflection->getAttributes(static::class)) {
return true;
}

return (bool)$classReflection->getMethod($method)->getAttributes(static::class);
}
}
18 changes: 18 additions & 0 deletions src/InMemory/CannotEnableInMemory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\InMemory;

final class CannotEnableInMemory extends \LogicException
{
public static function testIsNotAKernelTestCase(string $testName): self
{
return new self("{$testName}: Cannot use the #[AsInMemoryTest] attribute without extending KernelTestCase.");
}

public static function noInMemoryRepositoryRegistry(): self
{
return new self('Cannot enable "in memory": maybe not in a KernelTestCase?');
}
}
49 changes: 49 additions & 0 deletions src/InMemory/DependencyInjection/InMemoryCompilerPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\InMemory\DependencyInjection;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Zenstruck\Foundry\InMemory\InMemoryFactoryRegistry;

/**
* @internal
* @author Nicolas PHILIPPE <[email protected]>
*/
final class InMemoryCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
// create a service locator with all "in memory" repositories, indexed by target class
$inMemoryRepositoriesServices = $container->findTaggedServiceIds('foundry.in_memory.repository');
$inMemoryRepositoriesLocator = ServiceLocatorTagPass::register(
$container,
array_combine(
array_map(
static function (array $tags) {
if (\count($tags) !== 1) {
throw new \LogicException('Cannot have multiple tags "foundry.in_memory.repository" on a service!');
}

return $tags[0]['class'] ?? throw new \LogicException('Invalid tag definition of "foundry.in_memory.repository".');
},
array_values($inMemoryRepositoriesServices)
),
array_map(
static fn(string $inMemoryRepositoryId) => new Reference($inMemoryRepositoryId),
array_keys($inMemoryRepositoriesServices)
),
)
);

// todo: should we check we only have a 1 repository per class?

$container->findDefinition('.zenstruck_foundry.in_memory.repository_registry')
->setArgument('$inMemoryRepositories', $inMemoryRepositoriesLocator)
;
}
}
48 changes: 48 additions & 0 deletions src/InMemory/GenericInMemoryRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\InMemory;

/**
* @template T of object
* @implements InMemoryRepository<T>
* @author Nicolas PHILIPPE <[email protected]>
*
* This class will be used when a specific "in-memory" repository does not exist for a given class.
*/
final class GenericInMemoryRepository implements InMemoryRepository
{
/**
* @var list<T>
*/
private array $elements = [];

/**
* @param class-string<T> $class
*/
public function __construct(
private readonly string $class
)
{
}

/**
* @param T $element
*/
public function _save(object $element): void
{
if (!$element instanceof $this->class) {
throw new \InvalidArgumentException(sprintf('Given object of class "%s" is not an instance of expected "%s"', $element::class, $this->class));
}

if (!in_array($element, $this->elements, true)) {
$this->elements[] = $element;
}
}

public function _all(): array
{
return $this->elements;
}
}
Loading
Loading