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 May 1, 2024
1 parent 71bbaa2 commit 4add46a
Show file tree
Hide file tree
Showing 16 changed files with 427 additions and 14 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
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;
}
}
13 changes: 13 additions & 0 deletions src/Exception/CannotCreateFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\Exception;

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 <kevinbond@gmail.com>
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 <kevinbond@gmail.com>
*
* @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 <kevinbond@gmail.com>
*
* 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 <nikophil@gmail.com>
*
* @internal
*/
interface FactoryRegistryInterface
{
/**
* @template T of Factory
*
* @param class-string<T> $class
*
* @return T
*/
public function get(string $class): Factory;
}
19 changes: 19 additions & 0 deletions src/InMemory/AsInMemoryRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\InMemory;

// todo: remove this attribute in favor to interface?
#[\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.");
}
}
}
51 changes: 51 additions & 0 deletions src/InMemory/DependencyInjection/InMemoryCompilerPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?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
*/
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->register('.zenstruck_foundry.in_memory.factory_registry')
->setClass(InMemoryFactoryRegistry::class)
->setDecoratedService('.zenstruck_foundry.factory_registry')
->setArgument('$decorated', $container->getDefinition('.zenstruck_foundry.factory_registry'))
->setArgument('$inMemoryRepositories', $inMemoryRepositoriesLocator)
;
}
}
69 changes: 69 additions & 0 deletions src/InMemory/InMemoryFactoryRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\InMemory;

use Symfony\Component\DependencyInjection\ServiceLocator;
use Zenstruck\Foundry\Configuration;
use Zenstruck\Foundry\Factory;
use Zenstruck\Foundry\FactoryRegistryInterface;
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;

/**
* @internal
*/
final class InMemoryFactoryRegistry implements FactoryRegistryInterface
{
public function __construct( // @phpstan-ignore-line
private readonly FactoryRegistryInterface $decorated,
/** @var ServiceLocator<InMemoryRepository> */
private readonly ServiceLocator $inMemoryRepositories,
) {
}

public function get(string $class): Factory
{
$factory = $this->decorated->get($class);

if (!$factory instanceof PersistentObjectFactory) {
return $factory;
}

$configuration = Configuration::instance();

if (!$configuration->isInMemoryEnabled()) {
return $factory;
}

$factory = $factory->withoutPersisting();

if ($inMemoryRepository = $this->findInMemoryRepository($factory)) {
$factory = $factory->afterInstantiate(
static fn(object $object) => $inMemoryRepository->_save($object)
);
}

return $factory->withoutPersisting();
}

/**
* @template T of object
*
* @param PersistentObjectFactory<T> $factory
*
* @return InMemoryRepository<T>|null
*/
private function findInMemoryRepository(PersistentObjectFactory $factory): InMemoryRepository|null
{
$targetClass = $factory::class();
if (!$this->inMemoryRepositories->has($targetClass)) {
// todo: should we return null if no "in-memory" repository?
// ... or throw an exception?
// ... or provide a `GenericInMemoryRepository`?
return null;
}

return $this->inMemoryRepositories->get($targetClass);
}
}
16 changes: 16 additions & 0 deletions src/InMemory/InMemoryRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\InMemory;

/**
* @template T of object
*/
interface InMemoryRepository
{
/**
* @param T $element
*/
public function _save(object $element): void;
}
22 changes: 22 additions & 0 deletions src/InMemory/functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

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

namespace Zenstruck\Foundry\InMemory;

use Zenstruck\Foundry\Configuration;

/**
* Enable "in memory" repositories globally.
*/
function enable_in_memory(): void
{
Configuration::instance()->enableInMemory();
}
19 changes: 19 additions & 0 deletions src/ZenstruckFoundryBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@
namespace Zenstruck\Foundry;

use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
use Zenstruck\Foundry\InMemory\AsInMemoryRepository;
use Zenstruck\Foundry\InMemory\DependencyInjection\InMemoryCompilerPass;
use Zenstruck\Foundry\InMemory\InMemoryRepository;
use Zenstruck\Foundry\Object\Instantiator;
use Zenstruck\Foundry\ORM\AbstractORMPersistenceStrategy;

Expand Down Expand Up @@ -211,13 +215,28 @@ public function loadExtension(array $config, ContainerConfigurator $configurator
->replaceArgument(1, $config['mongo'])
;
}

// tag with "foundry.in_memory.repository" all classes using attribute "AsInMemoryRepository"
$container->registerAttributeForAutoconfiguration(
AsInMemoryRepository::class,
static function (ChildDefinition $definition, AsInMemoryRepository $attribute, \ReflectionClass $reflector) { // @phpstan-ignore-line
if (!is_a($reflector->name, InMemoryRepository::class, true)) {
throw new \LogicException(sprintf("Service \"%s\" with attribute \"AsInMemoryRepository\" must implement \"%s\".", $reflector->name, InMemoryRepository::class));
}

$definition->addTag('foundry.in_memory.repository', ['class' => $attribute->class]);
}
);
}

public function build(ContainerBuilder $container): void
{
parent::build($container);

$container->addCompilerPass($this);

// todo: should we find a way to decouple Foundry from its "plugins"?
$container->addCompilerPass(new InMemoryCompilerPass());
}

public function process(ContainerBuilder $container): void
Expand Down
Loading

0 comments on commit 4add46a

Please sign in to comment.