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 Jun 25, 2024
1 parent 1edf948 commit 51dac5b
Show file tree
Hide file tree
Showing 20 changed files with 560 additions and 15 deletions.
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@
},
"autoload": {
"psr-4": { "Zenstruck\\Foundry\\": "src/" },
"files": ["src/functions.php", "src/Persistence/functions.php", "src/phpunit_helper.php"]
"files": [
"src/functions.php",
"src/Persistence/functions.php",
"src/phpunit_helper.php",
"src/InMemory/functions.php"
]
},
"autoload-dev": {
"psr-4": {
Expand Down
17 changes: 17 additions & 0 deletions config/in_memory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?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'))
->arg('$inMemoryRepositoryRegistry', service('.zenstruck_foundry.in_memory.repository_registry'));

$container->services()
->set('.zenstruck_foundry.in_memory.repository_registry', InMemoryRepositoryRegistry::class);
};
14 changes: 13 additions & 1 deletion src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ final class Configuration
/** @var \Closure():self|self|null */
private static \Closure|self|null $instance = null;

private bool $inMemory = false;

/**
* @param InstantiatorCallable $instantiator
*/
public function __construct(
public readonly FactoryRegistry $factories,
public readonly FactoryRegistryInterface $factories,
public readonly Faker\Generator $faker,
callable $instantiator,
public readonly StoryRegistry $stories,
Expand Down Expand Up @@ -90,4 +92,14 @@ public static function shutdown(): void
StoryRegistry::reset();
self::$instance = null;
}

public function enableInMemory(): void
{
$this->inMemory = true;
}

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

declare(strict_types=1);

namespace Zenstruck\Foundry\Exception;

/**
* @author Nicolas PHILIPPE <[email protected]>
*/
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);
}
}
4 changes: 2 additions & 2 deletions 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 All @@ -33,7 +34,6 @@ public function __construct()
{
}


/**
* @param Attributes $attributes
*/
Expand All @@ -46,7 +46,7 @@ final public static function new(array|callable $attributes = []): static
try {
$factory ??= new static(); // @phpstan-ignore-line
} 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-line
}
}

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.");
}
}
}
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;
}
}
54 changes: 54 additions & 0 deletions src/InMemory/InMemoryFactoryRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\InMemory;

use Zenstruck\Foundry\Configuration;
use Zenstruck\Foundry\Factory;
use Zenstruck\Foundry\FactoryRegistryInterface;
use Zenstruck\Foundry\ObjectFactory;
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;

/**
* @internal
* @template T of object
* @author Nicolas PHILIPPE <[email protected]>
*/
final class InMemoryFactoryRegistry implements FactoryRegistryInterface
{
public function __construct( // @phpstan-ignore-line
private readonly FactoryRegistryInterface $decorated,
private readonly InMemoryRepositoryRegistry $inMemoryRepositoryRegistry,
) {
}

/**
* @template TFactory of Factory
*
* @param class-string<TFactory> $class
*
* @return TFactory
*/
public function get(string $class): Factory
{
$factory = $this->decorated->get($class);

if (!$factory instanceof ObjectFactory || !Configuration::instance()->isInMemoryEnabled()) {
return $factory;
}

$inMemoryRepository = $this->inMemoryRepositoryRegistry->get($factory::class());

if ($factory instanceof PersistentObjectFactory) {
$factory = $factory->withoutPersisting();
}

return $factory
->afterInstantiate(
function (object $object) use ($inMemoryRepository) {
$inMemoryRepository->_save($object);
}
);
}
}
23 changes: 23 additions & 0 deletions src/InMemory/InMemoryRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\InMemory;

/**
* @author Nicolas PHILIPPE <[email protected]>
*
* @template T of object
*/
interface InMemoryRepository
{
/**
* @param T $element
*/
public function _save(object $element): void;

/**
* @return list<T>
*/
public function _all(): array;
}
Loading

0 comments on commit 51dac5b

Please sign in to comment.