From 2b0f3a17bfe52752d7e9bd26bed93412c313dc5d Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 18 Jan 2025 16:09:19 +0100 Subject: [PATCH] feat: introduce hook services --- config/services.php | 7 ++ src/Configuration.php | 12 ++++ src/Hooks/AfterInstantiate.php | 42 +++++++++++ src/Hooks/AfterPersist.php | 42 +++++++++++ src/Hooks/AsAfterInstantiateFoundryHook.php | 26 +++++++ src/Hooks/AsAfterPersistFoundryHook.php | 26 +++++++ src/Hooks/AsBeforeInstantiateFoundryHook.php | 26 +++++++ src/Hooks/BeforeInstantiate.php | 42 +++++++++++ src/Hooks/HookEvent.php | 26 +++++++ src/Hooks/HooksRegistry.php | 59 +++++++++++++++ src/ObjectFactory.php | 29 ++++++++ src/Persistence/PersistentObjectFactory.php | 25 +++++-- src/ZenstruckFoundryBundle.php | 71 +++++++++++++++++-- tests/Fixture/Entity/EntityForHooks.php | 27 +++++++ tests/Fixture/Hooks/FactoryWithHook.php | 38 ++++++++++ .../Hooks/GlobalAfterInstantiateHook.php | 34 +++++++++ .../Fixture/Hooks/GlobalAfterPersistHook.php | 34 +++++++++ .../Hooks/GlobalBeforeInstantiateHook.php | 34 +++++++++ .../Hooks/ObjectAfterInstantiateHook.php | 30 ++++++++ .../Fixture/Hooks/ObjectAfterPersistHook.php | 30 ++++++++ .../Hooks/ObjectBeforeInstantiateHook.php | 30 ++++++++ tests/Fixture/TestKernel.php | 13 ++++ tests/Integration/Hooks/HooksTest.php | 46 ++++++++++++ 23 files changed, 738 insertions(+), 11 deletions(-) create mode 100644 src/Hooks/AfterInstantiate.php create mode 100644 src/Hooks/AfterPersist.php create mode 100644 src/Hooks/AsAfterInstantiateFoundryHook.php create mode 100644 src/Hooks/AsAfterPersistFoundryHook.php create mode 100644 src/Hooks/AsBeforeInstantiateFoundryHook.php create mode 100644 src/Hooks/BeforeInstantiate.php create mode 100644 src/Hooks/HookEvent.php create mode 100644 src/Hooks/HooksRegistry.php create mode 100644 tests/Fixture/Entity/EntityForHooks.php create mode 100644 tests/Fixture/Hooks/FactoryWithHook.php create mode 100644 tests/Fixture/Hooks/GlobalAfterInstantiateHook.php create mode 100644 tests/Fixture/Hooks/GlobalAfterPersistHook.php create mode 100644 tests/Fixture/Hooks/GlobalBeforeInstantiateHook.php create mode 100644 tests/Fixture/Hooks/ObjectAfterInstantiateHook.php create mode 100644 tests/Fixture/Hooks/ObjectAfterPersistHook.php create mode 100644 tests/Fixture/Hooks/ObjectBeforeInstantiateHook.php create mode 100644 tests/Integration/Hooks/HooksTest.php diff --git a/config/services.php b/config/services.php index cd3fd6fd..beb5697e 100644 --- a/config/services.php +++ b/config/services.php @@ -5,6 +5,7 @@ use Faker; use Zenstruck\Foundry\Configuration; use Zenstruck\Foundry\FactoryRegistry; +use Zenstruck\Foundry\Hooks\HooksRegistry; use Zenstruck\Foundry\Object\Instantiator; use Zenstruck\Foundry\StoryRegistry; @@ -32,7 +33,13 @@ service('.zenstruck_foundry.instantiator'), service('.zenstruck_foundry.story_registry'), service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(), + service('.zenstruck_foundry.hooks.registry')->nullOnInvalid(), ]) ->public() + + ->set('.zenstruck_foundry.hooks.registry', HooksRegistry::class) + ->args([ + abstract_arg('hooks_service_locator'), + ]) ; }; diff --git a/src/Configuration.php b/src/Configuration.php index 048604b4..2e4b8686 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -15,6 +15,7 @@ use Zenstruck\Foundry\Exception\FoundryNotBooted; use Zenstruck\Foundry\Exception\PersistenceDisabled; use Zenstruck\Foundry\Exception\PersistenceNotAvailable; +use Zenstruck\Foundry\Hooks\HooksRegistry; use Zenstruck\Foundry\Persistence\PersistenceManager; /** @@ -50,6 +51,7 @@ public function __construct( callable $instantiator, public readonly StoryRegistry $stories, private readonly ?PersistenceManager $persistence = null, + private readonly ?HooksRegistry $hooksRegistry = null, ) { $this->instantiator = $instantiator; } @@ -79,6 +81,16 @@ public function assertPersistenceEnabled(): void } } + public function isHooksRegistry(): bool + { + return (bool) $this->hooksRegistry; + } + + public function hooksRegistry(): HooksRegistry + { + return $this->hooksRegistry ?? throw new \LogicException('Cannot get hooks registry. Note: hooks cannot be used in unit tests.'); + } + public function inADataProvider(): bool { return $this->bootedForDataProvider; diff --git a/src/Hooks/AfterInstantiate.php b/src/Hooks/AfterInstantiate.php new file mode 100644 index 00000000..61323d3c --- /dev/null +++ b/src/Hooks/AfterInstantiate.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Hooks; + +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\ObjectFactory; + +/** + * @author Nicolas PHILIPPE + * + * @phpstan-import-type Parameters from Factory + * @template T of object + * @implements HookEvent + */ +final class AfterInstantiate implements HookEvent +{ + public function __construct( + /** @var T */ + public readonly object $object, + /** @phpstan-var Parameters */ + public readonly array $parameters, + /** @var ObjectFactory */ + public readonly ObjectFactory $factory, + ) { + } + + public function getObjectClass(): string + { + return $this->object::class; + } +} diff --git a/src/Hooks/AfterPersist.php b/src/Hooks/AfterPersist.php new file mode 100644 index 00000000..29960243 --- /dev/null +++ b/src/Hooks/AfterPersist.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Hooks; + +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; + +/** + * @author Nicolas PHILIPPE + * + * @phpstan-import-type Parameters from Factory + * @template T of object + * @implements HookEvent + */ +final class AfterPersist implements HookEvent +{ + public function __construct( + /** @var T */ + public readonly object $object, + /** @phpstan-var Parameters */ + public readonly array $parameters, + /** @var PersistentObjectFactory */ + public readonly PersistentObjectFactory $factory, + ) { + } + + public function getObjectClass(): string + { + return $this->object::class; + } +} diff --git a/src/Hooks/AsAfterInstantiateFoundryHook.php b/src/Hooks/AsAfterInstantiateFoundryHook.php new file mode 100644 index 00000000..7eefcde2 --- /dev/null +++ b/src/Hooks/AsAfterInstantiateFoundryHook.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Hooks; + +/** + * @author Nicolas PHILIPPE + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +final class AsAfterInstantiateFoundryHook +{ + public function __construct( + public ?string $class = null, + ) { + } +} diff --git a/src/Hooks/AsAfterPersistFoundryHook.php b/src/Hooks/AsAfterPersistFoundryHook.php new file mode 100644 index 00000000..bfc5b9c2 --- /dev/null +++ b/src/Hooks/AsAfterPersistFoundryHook.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Hooks; + +/** + * @author Nicolas PHILIPPE + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +final class AsAfterPersistFoundryHook +{ + public function __construct( + public ?string $class = null, + ) { + } +} diff --git a/src/Hooks/AsBeforeInstantiateFoundryHook.php b/src/Hooks/AsBeforeInstantiateFoundryHook.php new file mode 100644 index 00000000..cc209940 --- /dev/null +++ b/src/Hooks/AsBeforeInstantiateFoundryHook.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Hooks; + +/** + * @author Nicolas PHILIPPE + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +final class AsBeforeInstantiateFoundryHook +{ + public function __construct( + public ?string $class = null, + ) { + } +} diff --git a/src/Hooks/BeforeInstantiate.php b/src/Hooks/BeforeInstantiate.php new file mode 100644 index 00000000..879f1a46 --- /dev/null +++ b/src/Hooks/BeforeInstantiate.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Hooks; + +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\ObjectFactory; + +/** + * @author Nicolas PHILIPPE + * + * @phpstan-import-type Parameters from Factory + * @template T of object + * @implements HookEvent + */ +final class BeforeInstantiate implements HookEvent +{ + public function __construct( + /** @phpstan-var Parameters */ + public array $parameters, + /** @var class-string */ + public readonly string $objectClass, + /** @var ObjectFactory */ + public readonly ObjectFactory $factory, + ) { + } + + public function getObjectClass(): string + { + return $this->objectClass; + } +} diff --git a/src/Hooks/HookEvent.php b/src/Hooks/HookEvent.php new file mode 100644 index 00000000..240f8857 --- /dev/null +++ b/src/Hooks/HookEvent.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Hooks; + +/** + * @author Nicolas PHILIPPE + * + * @internal + * @template T of object + */ +interface HookEvent +{ + /** @return class-string */ + public function getObjectClass(): string; +} diff --git a/src/Hooks/HooksRegistry.php b/src/Hooks/HooksRegistry.php new file mode 100644 index 00000000..f18f4b17 --- /dev/null +++ b/src/Hooks/HooksRegistry.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Hooks; + +use Symfony\Component\DependencyInjection\ServiceLocator; + +/** + * @author Nicolas PHILIPPE + * + * @internal + */ +final class HooksRegistry +{ + public function __construct( + /** @var ServiceLocator): void>> */ + private ServiceLocator $serviceLocator, + ) { + } + + /** + * @param HookEvent $hookEvent + */ + public function callHooks(HookEvent $hookEvent): void + { + foreach ($this->resolveHooks($hookEvent) as $hook) { + ($hook)($hookEvent); + } + } + + public static function hookClassSpecificIndex(string $hookEventClass, string $objectClass): string + { + return "{$hookEventClass}-{$objectClass}"; + } + + /** + * @param HookEvent $hookEvent + * @return list): void> + */ + private function resolveHooks(HookEvent $hookEvent): array + { + $objectSpecificIndex = self::hookClassSpecificIndex($hookEvent::class, $hookEvent->getObjectClass()); + + return [ + ...$this->serviceLocator->has($hookEvent::class) ? $this->serviceLocator->get($hookEvent::class) : [], + ...$this->serviceLocator->has($objectSpecificIndex) ? $this->serviceLocator->get($objectSpecificIndex) : [], + ]; + } +} diff --git a/src/ObjectFactory.php b/src/ObjectFactory.php index 8d69933f..d00f120c 100644 --- a/src/ObjectFactory.php +++ b/src/ObjectFactory.php @@ -11,6 +11,8 @@ namespace Zenstruck\Foundry; +use Zenstruck\Foundry\Hooks\AfterInstantiate; +use Zenstruck\Foundry\Hooks\BeforeInstantiate; use Zenstruck\Foundry\Object\Instantiator; /** @@ -102,4 +104,31 @@ public function afterInstantiate(callable $callback): static return $clone; } + + /** + * @internal + */ + protected function initializeInternal(): static + { + if (!Configuration::instance()->isHooksRegistry()) { + return $this; + } + + return $this->beforeInstantiate( + static function(array $parameters, string $objectClass, self $usedFactory): array { + Configuration::instance()->hooksRegistry()->callHooks( + $hook = new BeforeInstantiate($parameters, $objectClass, $usedFactory) + ); + + return $hook->parameters; + } + ) + ->afterInstantiate( + static function(object $object, array $parameters, self $usedFactory): void { + Configuration::instance()->hooksRegistry()->callHooks( + new AfterInstantiate($object, $parameters, $usedFactory) + ); + } + ); + } } diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 7e023d39..125fd881 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -18,6 +18,7 @@ use Zenstruck\Foundry\Exception\PersistenceNotAvailable; use Zenstruck\Foundry\Factory; use Zenstruck\Foundry\FactoryCollection; +use Zenstruck\Foundry\Hooks\AfterPersist; use Zenstruck\Foundry\ObjectFactory; use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects; use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; @@ -384,13 +385,27 @@ final protected function isPersisting(): bool */ final protected function initializeInternal(): static { - return $this->afterInstantiate( - static function(object $object, array $parameters, PersistentObjectFactory $factory): void { - if (!$factory->isPersisting() && (!isset($factory->persist) || PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT !== $factory->persist)) { - return; + $factory = parent::initializeInternal() + ->afterInstantiate( + static function(object $object, array $parameters, PersistentObjectFactory $factoryUsed): void { + if (!$factoryUsed->isPersisting( + ) && (!isset($factoryUsed->persist) || PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT !== $factoryUsed->persist)) { + return; + } + + Configuration::instance()->persistence()->scheduleForInsert($object); } + ); - Configuration::instance()->persistence()->scheduleForInsert($object); + if (!Configuration::instance()->isHooksRegistry()) { + return $factory; + } + + return $factory->afterPersist( + static function(object $object, array $parameters, self $factoryUsed): void { + Configuration::instance()->hooksRegistry()->callHooks( + new AfterPersist($object, $parameters, $factoryUsed) + ); } ); } diff --git a/src/ZenstruckFoundryBundle.php b/src/ZenstruckFoundryBundle.php index 21dc8445..7d9e6c48 100644 --- a/src/ZenstruckFoundryBundle.php +++ b/src/ZenstruckFoundryBundle.php @@ -12,11 +12,21 @@ namespace Zenstruck\Foundry; use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; +use Zenstruck\Foundry\Hooks\AfterInstantiate; +use Zenstruck\Foundry\Hooks\AfterPersist; +use Zenstruck\Foundry\Hooks\AsAfterInstantiateFoundryHook; +use Zenstruck\Foundry\Hooks\AsAfterPersistFoundryHook; +use Zenstruck\Foundry\Hooks\AsBeforeInstantiateFoundryHook; +use Zenstruck\Foundry\Hooks\BeforeInstantiate; +use Zenstruck\Foundry\Hooks\HooksRegistry; use Zenstruck\Foundry\Mongo\MongoResetter; use Zenstruck\Foundry\Object\Instantiator; use Zenstruck\Foundry\ORM\ResetDatabase\OrmResetter; @@ -192,13 +202,46 @@ public function configure(DefinitionConfigurator $definition): void public function loadExtension(array $config, ContainerConfigurator $configurator, ContainerBuilder $container): void // @phpstan-ignore missingType.iterableValue { - $container->registerForAutoconfiguration(Factory::class) - ->addTag('foundry.factory') - ; + $container->registerForAutoconfiguration(Factory::class)->addTag('foundry.factory'); + + $container->registerForAutoconfiguration(Story::class)->addTag('foundry.story'); + + $hookValidator = static function(\ReflectionClass $reflector, string $hookEventClass, object $attribute): void { + if ( + !$reflector->hasMethod('__invoke') + || 1 !== \count($parameters = $reflector->getMethod('__invoke')->getParameters()) + || !($typeFirstParameter = $parameters[0]->getType()) + || (string) $typeFirstParameter !== $hookEventClass + ) { + throw new InvalidArgumentException(\sprintf('In order to be declared "#[%s]", the class "%s" must have an "__invoke" method with one unique parameter of type "%s".', (new \ReflectionClass($attribute))->getShortName(), $reflector->getName(), $hookEventClass)); + } + }; + + $container->registerAttributeForAutoconfiguration( + AsBeforeInstantiateFoundryHook::class, + // @phpstan-ignore argument.type + static function(ChildDefinition $definition, AsBeforeInstantiateFoundryHook $attribute, \ReflectionClass $reflector) use ($hookValidator) { + $hookValidator($reflector, BeforeInstantiate::class, $attribute); + $definition->addTag('foundry.hook', ['hook' => BeforeInstantiate::class, 'class' => $attribute->class]); + }); + + $container->registerAttributeForAutoconfiguration( + AsAfterInstantiateFoundryHook::class, + // @phpstan-ignore argument.type + static function(ChildDefinition $definition, AsAfterInstantiateFoundryHook $attribute, \ReflectionClass $reflector) use ($hookValidator) { + $hookValidator($reflector, AfterInstantiate::class, $attribute); + $definition->addTag('foundry.hook', ['hook' => AfterInstantiate::class, 'class' => $attribute->class]); + } + ); - $container->registerForAutoconfiguration(Story::class) - ->addTag('foundry.story') - ; + $container->registerAttributeForAutoconfiguration( + AsAfterPersistFoundryHook::class, + // @phpstan-ignore argument.type + static function(ChildDefinition $definition, AsAfterPersistFoundryHook $attribute, \ReflectionClass $reflector) use ($hookValidator) { + $hookValidator($reflector, AfterPersist::class, $attribute); + $definition->addTag('foundry.hook', ['hook' => AfterPersist::class, 'class' => $attribute->class]); + } + ); $configurator->import('../config/services.php'); @@ -294,6 +337,22 @@ public function process(ContainerBuilder $container): void ->addMethodCall('addProvider', [new Reference($id)]) ; } + + // hooks + $iterators = []; + foreach ($container->findTaggedServiceIds('foundry.hook') as $id => $tags) { + $index = isset($tags[0]['class']) + ? HooksRegistry::hookClassSpecificIndex($tags[0]['hook'], $tags[0]['class']) + : $tags[0]['hook']; + + $iterators[$index] ??= []; + $iterators[$index][] = new Reference($id); + } + + $container + ->getDefinition('.zenstruck_foundry.hooks.registry') + ->replaceArgument(0, new ServiceLocatorArgument($iterators)) + ; } /** diff --git a/tests/Fixture/Entity/EntityForHooks.php b/tests/Fixture/Entity/EntityForHooks.php new file mode 100644 index 00000000..73dc491a --- /dev/null +++ b/tests/Fixture/Entity/EntityForHooks.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; + +#[ORM\Entity] +class EntityForHooks extends Base +{ + public function __construct( + #[ORM\Column()] + public string $name, + ) { + } +} diff --git a/tests/Fixture/Hooks/FactoryWithHook.php b/tests/Fixture/Hooks/FactoryWithHook.php new file mode 100644 index 00000000..133a0b84 --- /dev/null +++ b/tests/Fixture/Hooks/FactoryWithHook.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Hooks; + +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForHooks; + +/** + * @extends PersistentObjectFactory + */ +final class FactoryWithHook extends PersistentObjectFactory +{ + public static function class(): string + { + return EntityForHooks::class; + } + + /** + * @return array + */ + protected function defaults(): array + { + return [ + 'name' => self::faker()->sentence(), + ]; + } +} diff --git a/tests/Fixture/Hooks/GlobalAfterInstantiateHook.php b/tests/Fixture/Hooks/GlobalAfterInstantiateHook.php new file mode 100644 index 00000000..91800153 --- /dev/null +++ b/tests/Fixture/Hooks/GlobalAfterInstantiateHook.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Hooks; + +use Zenstruck\Foundry\Hooks\AfterInstantiate; +use Zenstruck\Foundry\Hooks\AsAfterInstantiateFoundryHook; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForHooks; + +#[AsAfterInstantiateFoundryHook] +final class GlobalAfterInstantiateHook +{ + /** + * @param AfterInstantiate $hook + */ + public function __invoke(AfterInstantiate $hook): void + { + if (!$hook->object instanceof EntityForHooks) { + return; + } + + $hook->object->name = "{$hook->object->name}\nGlobalAfterInstantiateHook"; + } +} diff --git a/tests/Fixture/Hooks/GlobalAfterPersistHook.php b/tests/Fixture/Hooks/GlobalAfterPersistHook.php new file mode 100644 index 00000000..05072098 --- /dev/null +++ b/tests/Fixture/Hooks/GlobalAfterPersistHook.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Hooks; + +use Zenstruck\Foundry\Hooks\AfterPersist; +use Zenstruck\Foundry\Hooks\AsAfterPersistFoundryHook; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForHooks; + +#[AsAfterPersistFoundryHook] +final class GlobalAfterPersistHook +{ + /** + * @param AfterPersist $hook + */ + public function __invoke(AfterPersist $hook): void + { + if (!$hook->object instanceof EntityForHooks) { + return; + } + + $hook->object->name = "{$hook->object->name}\nGlobalAfterPersistHook"; + } +} diff --git a/tests/Fixture/Hooks/GlobalBeforeInstantiateHook.php b/tests/Fixture/Hooks/GlobalBeforeInstantiateHook.php new file mode 100644 index 00000000..5cbaa880 --- /dev/null +++ b/tests/Fixture/Hooks/GlobalBeforeInstantiateHook.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Hooks; + +use Zenstruck\Foundry\Hooks\AsBeforeInstantiateFoundryHook; +use Zenstruck\Foundry\Hooks\BeforeInstantiate; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForHooks; + +#[AsBeforeInstantiateFoundryHook()] +final class GlobalBeforeInstantiateHook +{ + /** + * @param BeforeInstantiate $hook + */ + public function __invoke(BeforeInstantiate $hook): void + { + if (EntityForHooks::class !== $hook->objectClass) { + return; + } + + $hook->parameters['name'] = "{$hook->parameters['name']}\nGlobalBeforeInstantiateHook"; + } +} diff --git a/tests/Fixture/Hooks/ObjectAfterInstantiateHook.php b/tests/Fixture/Hooks/ObjectAfterInstantiateHook.php new file mode 100644 index 00000000..87871bf8 --- /dev/null +++ b/tests/Fixture/Hooks/ObjectAfterInstantiateHook.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Hooks; + +use Zenstruck\Foundry\Hooks\AfterInstantiate; +use Zenstruck\Foundry\Hooks\AsAfterInstantiateFoundryHook; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForHooks; + +#[AsAfterInstantiateFoundryHook(class: EntityForHooks::class)] +final class ObjectAfterInstantiateHook +{ + /** + * @param AfterInstantiate $hook + */ + public function __invoke(AfterInstantiate $hook): void + { + $hook->object->name = "{$hook->object->name}\nObjectAfterInstantiateHook"; + } +} diff --git a/tests/Fixture/Hooks/ObjectAfterPersistHook.php b/tests/Fixture/Hooks/ObjectAfterPersistHook.php new file mode 100644 index 00000000..34a4ffdb --- /dev/null +++ b/tests/Fixture/Hooks/ObjectAfterPersistHook.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Hooks; + +use Zenstruck\Foundry\Hooks\AfterPersist; +use Zenstruck\Foundry\Hooks\AsAfterPersistFoundryHook; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForHooks; + +#[AsAfterPersistFoundryHook(class: EntityForHooks::class)] +final class ObjectAfterPersistHook +{ + /** + * @param AfterPersist $hook + */ + public function __invoke(AfterPersist $hook): void + { + $hook->object->name = "{$hook->object->name}\nObjectAfterPersistHook"; + } +} diff --git a/tests/Fixture/Hooks/ObjectBeforeInstantiateHook.php b/tests/Fixture/Hooks/ObjectBeforeInstantiateHook.php new file mode 100644 index 00000000..ecc00e45 --- /dev/null +++ b/tests/Fixture/Hooks/ObjectBeforeInstantiateHook.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Hooks; + +use Zenstruck\Foundry\Hooks\AsBeforeInstantiateFoundryHook; +use Zenstruck\Foundry\Hooks\BeforeInstantiate; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForHooks; + +#[AsBeforeInstantiateFoundryHook(class: EntityForHooks::class)] +final class ObjectBeforeInstantiateHook +{ + /** + * @param BeforeInstantiate $hook + */ + public function __invoke(BeforeInstantiate $hook): void + { + $hook->parameters['name'] = "{$hook->parameters['name']}\nObjectBeforeInstantiateHook"; + } +} diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index 50ae9529..131634aa 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -27,6 +27,12 @@ use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangeCascadePersistOnLoadClassMetadataListener; use Zenstruck\Foundry\Tests\Fixture\Factories\ArrayFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; +use Zenstruck\Foundry\Tests\Fixture\Hooks\GlobalAfterInstantiateHook; +use Zenstruck\Foundry\Tests\Fixture\Hooks\GlobalAfterPersistHook; +use Zenstruck\Foundry\Tests\Fixture\Hooks\GlobalBeforeInstantiateHook; +use Zenstruck\Foundry\Tests\Fixture\Hooks\ObjectAfterInstantiateHook; +use Zenstruck\Foundry\Tests\Fixture\Hooks\ObjectAfterPersistHook; +use Zenstruck\Foundry\Tests\Fixture\Hooks\ObjectBeforeInstantiateHook; use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalInvokableService; use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalStory; use Zenstruck\Foundry\Tests\Fixture\Stories\ServiceStory; @@ -163,6 +169,13 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load $c->register(ArrayFactory::class)->setAutowired(true)->setAutoconfigured(true); $c->register(Object1Factory::class)->setAutowired(true)->setAutoconfigured(true); $c->register(ServiceStory::class)->setAutowired(true)->setAutoconfigured(true); + + $c->register(GlobalBeforeInstantiateHook::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(ObjectBeforeInstantiateHook::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(GlobalAfterInstantiateHook::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(ObjectAfterInstantiateHook::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(GlobalAfterPersistHook::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(ObjectAfterPersistHook::class)->setAutowired(true)->setAutoconfigured(true); } protected function configureRoutes(RoutingConfigurator $routes): void diff --git a/tests/Integration/Hooks/HooksTest.php b/tests/Integration/Hooks/HooksTest.php new file mode 100644 index 00000000..170ddd22 --- /dev/null +++ b/tests/Integration/Hooks/HooksTest.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\Hooks; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Hooks\FactoryWithHook; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +final class HooksTest extends KernelTestCase +{ + use Factories, RequiresORM, ResetDatabase; + + /** + * @test + */ + public function it_can_call_hooks(): void + { + $address = FactoryWithHook::createOne(['name' => 'hooks']); + + self::assertSame( + <<name + ); + } +}