Skip to content

Commit

Permalink
feat: introduce "in-memory" behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
nikophil committed Oct 26, 2024
1 parent dfeb247 commit 7608b78
Show file tree
Hide file tree
Showing 30 changed files with 741 additions and 69 deletions.
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

0 comments on commit 7608b78

Please sign in to comment.