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 Apr 9, 2024
1 parent ec2c895 commit 3e88d34
Show file tree
Hide file tree
Showing 18 changed files with 406 additions and 13 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
},
"autoload": {
"psr-4": { "Zenstruck\\Foundry\\": "src/" },
"files": ["src/functions.php", "src/Persistence/functions.php"]
"files": ["src/functions.php", "src/Persistence/functions.php", "src/InMemory/functions.php"]
},
"autoload-dev": {
"psr-4": {
Expand Down
1 change: 1 addition & 0 deletions phpunit.dama.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<ini name="error_reporting" value="-1" />
<server name="KERNEL_CLASS" value="Zenstruck\Foundry\Tests\Fixture\TestKernel" />
<server name="SHELL_VERBOSITY" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0&amp;quiet[]=indirect&amp;quiet[]=other&amp;ignoreFile=./tests/baseline-ignore"/>
<env name="USE_DAMA_DOCTRINE_TEST_BUNDLE" value="1"/>
</php>
Expand Down
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<ini name="error_reporting" value="-1" />
<server name="KERNEL_CLASS" value="Zenstruck\Foundry\Tests\Fixture\TestKernel" />
<server name="SHELL_VERBOSITY" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0&amp;quiet[]=indirect&amp;quiet[]=other&amp;ignoreFile=./tests/baseline-ignore"/>
</php>

Expand Down
2 changes: 1 addition & 1 deletion src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ final class Configuration
* @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
1 change: 0 additions & 1 deletion src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public function __construct()
{
}


/**
* @param Attributes $attributes
*/
Expand Down
13 changes: 3 additions & 10 deletions src/FactoryRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*
* @internal
*/
final class FactoryRegistry
final class FactoryRegistry implements FactoryRegistryInterface
{
/**
* @param Factory<mixed>[] $factories
Expand All @@ -25,21 +25,14 @@ 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;
return new $class();
}
}
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)
;
}
}
72 changes: 72 additions & 0 deletions src/InMemory/InMemoryFactoryRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?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 (!is_a($class, PersistentObjectFactory::class, allow_string: true)) {
return $factory;
}

$configuration = Configuration::instance();

if (
!$factory instanceof PersistentObjectFactory
|| !$configuration->isPersistenceAvailable()
|| !$configuration->persistence()->isInMemoryEnabled()
) {
return $factory;
}

$factory = $factory->withoutPersisting();

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

return $factory->withoutPersisting();
}

/**
* @template T of object
*
* @param class-string<PersistentObjectFactory<T>> $class
*
* @return InMemoryRepository<T>|null
*/
private function findInMemoryRepository(string $class): InMemoryRepository|null
{
$targetClass = $class::class();
if (!$this->inMemoryRepositories->has($targetClass)) {
// todo: should this behavior be opt-in from bundle's configuration?
// ie: throwing here would warn if a class does not have a "in memory" repository
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()->persistence()->enableInMemory();
}
12 changes: 12 additions & 0 deletions src/Persistence/PersistenceManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ final class PersistenceManager

private bool $flush = true;
private bool $persist = true;
private bool $inMemory = false;

/**
* @param PersistenceStrategy[] $strategies
Expand Down Expand Up @@ -153,6 +154,17 @@ public function enablePersisting(): void
$this->persist = true;
}

public function isInMemoryEnabled(): bool
{
return $this->inMemory;
}

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

/**
* @template T of object
*
Expand Down
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
36 changes: 36 additions & 0 deletions tests/Fixture/InMemory/InMemoryStandardAddressRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\Tests\Fixture\InMemory;

use Zenstruck\Foundry\InMemory\AsInMemoryRepository;
use Zenstruck\Foundry\InMemory\InMemoryRepository;
use Zenstruck\Foundry\Tests\Fixture\Entity\Address\StandardAddress;

/**
* @implements InMemoryRepository<StandardAddress>
*/
#[AsInMemoryRepository(class: StandardAddress::class)]
final class InMemoryStandardAddressRepository implements InMemoryRepository
{
/**
* @var list<StandardAddress>
*/
private array $elements = [];

public function _save(object $element): void
{
if (!in_array($element, $this->elements, true)) {
$this->elements[] = $element;
}
}

/**
* @return list<StandardAddress>
*/
public function all(): array
{
return $this->elements;
}
}
Loading

0 comments on commit 3e88d34

Please sign in to comment.