diff --git a/composer.json b/composer.json index 6b74cfb1..c421f906 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "phpunit/phpunit": "^9.5.0 || ^10.0 || ^11.0", "symfony/console": "^6.4|^7.0", "symfony/dotenv": "^6.4|^7.0", + "symfony/event-dispatcher": "^7.2", "symfony/maker-bundle": "^1.55", "symfony/phpunit-bridge": "^6.4|^7.0", "symfony/runtime": "^6.4|^7.0", 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..72aa71cb 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('noop!'); + } + 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..21a3aaa1 --- /dev/null +++ b/src/Hooks/AfterInstantiate.php @@ -0,0 +1,40 @@ + + * + * 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; + +/** + * @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..a68ca1b4 --- /dev/null +++ b/src/Hooks/AfterPersist.php @@ -0,0 +1,40 @@ + + * + * 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; + +/** + * @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..75a20013 --- /dev/null +++ b/src/Hooks/AsAfterInstantiateFoundryHook.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Hooks; + +#[\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..d07e3159 --- /dev/null +++ b/src/Hooks/AsAfterPersistFoundryHook.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Hooks; + +#[\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..7c8d61a0 --- /dev/null +++ b/src/Hooks/AsBeforeInstantiateFoundryHook.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Hooks; + +#[\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..550ee9ec --- /dev/null +++ b/src/Hooks/BeforeInstantiate.php @@ -0,0 +1,40 @@ + + * + * 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; + +/** + * @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..0b861636 --- /dev/null +++ b/src/Hooks/HookEvent.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Hooks; + +/** + * todo: check if this is possible. + * @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..0a3eda92 --- /dev/null +++ b/src/Hooks/HooksRegistry.php @@ -0,0 +1,57 @@ + + * + * 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; + +/** + * @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 (callable(HookEvent): 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..835552a9 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,15 +385,29 @@ 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)) { + $factory = parent::initializeInternal(); + + $factory = $factory->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); } ); + + if (!Configuration::instance()->isHooksRegistry()) { + return $factory; + } + + return $factory->afterPersist( + static function(object $object, array $parameters, self $factory): void { + Configuration::instance()->hooksRegistry()->callHooks( + new AfterPersist($object, $parameters, $factory) + ); + } + ); } private function withoutPersistingButScheduleForInsert(): static diff --git a/src/ZenstruckFoundryBundle.php b/src/ZenstruckFoundryBundle.php index 21dc8445..5054bac1 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,53 @@ 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') + || count($parameters = $reflector->getMethod('__invoke')->getParameters()) !== 1 + || !($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 +344,27 @@ public function process(ContainerBuilder $container): void ->addMethodCall('addProvider', [new Reference($id)]) ; } + + // hooks + $iterators = []; + foreach ($container->findTaggedServiceIds('foundry.hook') as $id => $tags) { + if (isset($tags[0]['class'])) { + $objectSpecificIndex = HooksRegistry::hookClassSpecificIndex($tags[0]['hook'], $tags[0]['class']); + + $iterators[$objectSpecificIndex] ??= []; + $iterators[$objectSpecificIndex][] = new Reference($id); + + continue; + } + + $iterators[$tags[0]['hook']] ??= []; + $iterators[$tags[0]['hook']][] = new Reference($id); + } + + $container + ->getDefinition('.zenstruck_foundry.hooks.registry') + ->replaceArgument(0, new ServiceLocatorArgument($iterators)) + ; } /** diff --git a/tests/Fixture/Hooks/AddressAfterInstantiateHook.php b/tests/Fixture/Hooks/AddressAfterInstantiateHook.php new file mode 100644 index 00000000..3b734896 --- /dev/null +++ b/tests/Fixture/Hooks/AddressAfterInstantiateHook.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\Address; + +#[AsAfterInstantiateFoundryHook(class: Address::class)] +final class AddressAfterInstantiateHook +{ + /** + * @param AfterInstantiate
$hook + */ + public function __invoke(AfterInstantiate $hook): void + { + if ($hook->factory instanceof AddressFactoryWithHook) { + $hook->object->setCity( + 'AddressAfterInitialisationHook '.$hook->object->getCity() + ); + } + } +} diff --git a/tests/Fixture/Hooks/AddressAfterPersistHook.php b/tests/Fixture/Hooks/AddressAfterPersistHook.php new file mode 100644 index 00000000..ef657be8 --- /dev/null +++ b/tests/Fixture/Hooks/AddressAfterPersistHook.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\Address; + +#[AsAfterPersistFoundryHook(class: Address::class)] +final class AddressAfterPersistHook +{ + /** + * @param AfterPersist
$hook + */ + public function __invoke(AfterPersist $hook): void + { + if ($hook->factory instanceof AddressFactoryWithHook) { + $hook->object->setCity( + 'AddressAfterPersistHook '.$hook->object->getCity() + ); + } + } +} diff --git a/tests/Fixture/Hooks/AddressBeforeInstantiateHook.php b/tests/Fixture/Hooks/AddressBeforeInstantiateHook.php new file mode 100644 index 00000000..265ddfbe --- /dev/null +++ b/tests/Fixture/Hooks/AddressBeforeInstantiateHook.php @@ -0,0 +1,32 @@ + + * + * 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\Address; + +#[AsBeforeInstantiateFoundryHook(class: Address::class)] +final class AddressBeforeInstantiateHook +{ + /** + * @param BeforeInstantiate
$hook + */ + public function __invoke(BeforeInstantiate $hook): void + { + if ($hook->factory instanceof AddressFactoryWithHook) { + $hook->parameters['city'] = "AddressBeforeInstantiateHook {$hook->parameters['city']}"; + } + } +} diff --git a/tests/Fixture/Hooks/AddressFactoryWithHook.php b/tests/Fixture/Hooks/AddressFactoryWithHook.php new file mode 100644 index 00000000..fba67a6d --- /dev/null +++ b/tests/Fixture/Hooks/AddressFactoryWithHook.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\Address; + +/** + * @extends PersistentObjectFactory
+ */ +final class AddressFactoryWithHook extends PersistentObjectFactory +{ + public static function class(): string + { + return Address::class; + } + + /** + * @return array + */ + protected function defaults(): array + { + return [ + 'city' => self::faker()->city(), + ]; + } +} diff --git a/tests/Fixture/Hooks/AfterInstantiateHook.php b/tests/Fixture/Hooks/AfterInstantiateHook.php new file mode 100644 index 00000000..b7229814 --- /dev/null +++ b/tests/Fixture/Hooks/AfterInstantiateHook.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\Address; + +#[AsAfterInstantiateFoundryHook] +final class AfterInstantiateHook +{ + /** + * @param AfterInstantiate
$hook + */ + public function __invoke(AfterInstantiate $hook): void + { + if ($hook->factory instanceof AddressFactoryWithHook) { + $hook->object->setCity( + 'AfterInitialisationHook '.$hook->object->getCity() + ); + } + } +} diff --git a/tests/Fixture/Hooks/AfterPersistHook.php b/tests/Fixture/Hooks/AfterPersistHook.php new file mode 100644 index 00000000..a282c69f --- /dev/null +++ b/tests/Fixture/Hooks/AfterPersistHook.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\Address; + +#[AsAfterPersistFoundryHook] +final class AfterPersistHook +{ + /** + * @param AfterPersist
$hook + */ + public function __invoke(AfterPersist $hook): void + { + if ($hook->factory instanceof AddressFactoryWithHook) { + $hook->object->setCity( + 'AfterPersistHook '.$hook->object->getCity() + ); + } + } +} diff --git a/tests/Fixture/Hooks/BeforeInstantiateHook.php b/tests/Fixture/Hooks/BeforeInstantiateHook.php new file mode 100644 index 00000000..96140211 --- /dev/null +++ b/tests/Fixture/Hooks/BeforeInstantiateHook.php @@ -0,0 +1,32 @@ + + * + * 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\Address; + +#[AsBeforeInstantiateFoundryHook()] +final class BeforeInstantiateHook +{ + /** + * @param BeforeInstantiate
$hook + */ + public function __invoke(BeforeInstantiate $hook): void + { + if ($hook->factory instanceof AddressFactoryWithHook) { + $hook->parameters['city'] = "BeforeInstantiateHook {$hook->parameters['city']}"; + } + } +} diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index 50ae9529..34006714 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\AddressAfterInstantiateHook; +use Zenstruck\Foundry\Tests\Fixture\Hooks\AddressAfterPersistHook; +use Zenstruck\Foundry\Tests\Fixture\Hooks\AddressBeforeInstantiateHook; +use Zenstruck\Foundry\Tests\Fixture\Hooks\AfterInstantiateHook; +use Zenstruck\Foundry\Tests\Fixture\Hooks\AfterPersistHook; +use Zenstruck\Foundry\Tests\Fixture\Hooks\BeforeInstantiateHook; 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(BeforeInstantiateHook::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(AddressBeforeInstantiateHook::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(AfterInstantiateHook::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(AddressAfterInstantiateHook::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(AfterPersistHook::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(AddressAfterPersistHook::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..9396e016 --- /dev/null +++ b/tests/Integration/Hooks/HooksTest.php @@ -0,0 +1,37 @@ + + * + * 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\AddressFactoryWithHook; + +final class HooksTest extends KernelTestCase +{ + use Factories, ResetDatabase; + + /** + * @test + */ + public function it_can_call_hooks(): void + { + $address = AddressFactoryWithHook::createOne(['city' => 'hooks']); + + self::assertSame( + 'AddressAfterPersistHook AfterPersistHook AddressAfterInitialisationHook AfterInitialisationHook AddressBeforeInstantiateHook BeforeInstantiateHook hooks', + $address->getCity() + ); + } +}