From 974ecb4cf361e399ce3e162bdf657a8fe38085a6 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Wed, 24 Sep 2025 21:36:12 +0200 Subject: [PATCH 1/2] feat: introduce attribute #[AsFoudryHook] --- docs/index.rst | 29 ++++---- src/Attribute/AsFoundryHook.php | 28 ++++++++ src/Object/Event/AfterInstantiate.php | 13 +++- src/Object/Event/BeforeInstantiate.php | 14 +++- src/Object/Event/Event.php | 25 +++++++ src/Object/Event/HookListenerFilter.php | 45 ++++++++++++ src/Persistence/Event/AfterPersist.php | 14 +++- src/ZenstruckFoundryBundle.php | 38 ++++++++++ tests/Fixture/Events/FoundryEventListener.php | 72 ++++++++++++++++++- .../{Persistence => }/EventsTest.php | 9 ++- 10 files changed, 263 insertions(+), 24 deletions(-) create mode 100644 src/Attribute/AsFoundryHook.php create mode 100644 src/Object/Event/Event.php create mode 100644 src/Object/Event/HookListenerFilter.php rename tests/Integration/{Persistence => }/EventsTest.php (75%) diff --git a/docs/index.rst b/docs/index.rst index d8cf0683f..ea89bc211 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -664,11 +664,11 @@ You can also add hooks directly in your factory class: Read `Initialization`_ to learn more about the ``initialize()`` method. -Events -~~~~~~ +Hooks as service / global hooks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In addition to hooks, Foundry also leverages `symfony/event-dispatcher` and dispatches events that you can listen to, -allowing to create hooks globally, as Symfony services: +For a better control of your hooks, you can define them as services, allowing to leverage dependency injection and +to create hooks globally: :: @@ -677,26 +677,26 @@ allowing to create hooks globally, as Symfony services: use Zenstruck\Foundry\Object\Event\BeforeInstantiate; use Zenstruck\Foundry\Persistence\Event\AfterPersist; - final class FoundryEventListener + final class FoundryHook { - #[AsEventListener] + #[AsFoundryHook(Post::class)] public function beforeInstantiate(BeforeInstantiate $event): void { - // do something before the object is instantiated: + // do something before the post is instantiated: // $event->parameters is what will be used to instantiate the object, manipulate as required // $event->objectClass is the class of the object being instantiated // $event->factory is the factory instance which creates the object } - #[AsEventListener] + #[AsFoundryHook(Post::class)] public function afterInstantiate(AfterInstantiate $event): void { - // $event->object is the instantiated object + // $event->object is the instantiated Post object // $event->parameters contains the attributes used to instantiate the object and any extras // $event->factory is the factory instance which creates the object } - #[AsEventListener] + #[AsFoundryHook(Post::class)] public function afterPersist(AfterPersist $event): void { // this event is only called if the object was persisted @@ -704,12 +704,17 @@ allowing to create hooks globally, as Symfony services: // $event->parameters contains the attributes used to instantiate the object and any extras // $event->factory is the factory instance which creates the object } + + #[AsFoundryHook] + public function afterInstantiateGlobal(AfterInstantiate $event): void + { + // Omitting class defines a "global" hook which will be called for all objects + } } .. versionadded:: 2.8 - Those events are triggered since Foundry 2.8. - + The ``#[AsFoundryHook]`` attribute was added in Foundry 2.8. .. note:: If you want to save data to the database in an ``AfterPersist`` listener, Foundry won't flush automatically, and you diff --git a/src/Attribute/AsFoundryHook.php b/src/Attribute/AsFoundryHook.php new file mode 100644 index 000000000..68ff14e16 --- /dev/null +++ b/src/Attribute/AsFoundryHook.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Attribute; + +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + +#[\Attribute(\Attribute::TARGET_METHOD)] +final class AsFoundryHook extends AsEventListener +{ + public function __construct( + /** @var class-string */ + public readonly ?string $objectClass = null, + int $priority = 0, + ) { + parent::__construct(priority: $priority); + } +} diff --git a/src/Object/Event/AfterInstantiate.php b/src/Object/Event/AfterInstantiate.php index 7bbef74f5..356256531 100644 --- a/src/Object/Event/AfterInstantiate.php +++ b/src/Object/Event/AfterInstantiate.php @@ -19,16 +19,25 @@ /** * @author Nicolas PHILIPPE * + * @template T of object + * @implements Event + * * @phpstan-import-type Parameters from Factory */ -final class AfterInstantiate +final class AfterInstantiate implements Event { public function __construct( + /** @var T */ public readonly object $object, /** @phpstan-var Parameters */ public readonly array $parameters, - /** @var ObjectFactory */ + /** @var ObjectFactory */ public readonly ObjectFactory $factory, ) { } + + public function objectClassName(): string + { + return $this->object::class; + } } diff --git a/src/Object/Event/BeforeInstantiate.php b/src/Object/Event/BeforeInstantiate.php index b5ad5cb38..a79174056 100644 --- a/src/Object/Event/BeforeInstantiate.php +++ b/src/Object/Event/BeforeInstantiate.php @@ -19,17 +19,25 @@ /** * @author Nicolas PHILIPPE * + * @template T of object + * @implements Event + * * @phpstan-import-type Parameters from Factory */ -final class BeforeInstantiate +final class BeforeInstantiate implements Event { public function __construct( /** @phpstan-var Parameters */ public array $parameters, - /** @var class-string */ + /** @var class-string */ public readonly string $objectClass, - /** @var ObjectFactory */ + /** @var ObjectFactory */ public readonly ObjectFactory $factory, ) { } + + public function objectClassName(): string + { + return $this->objectClass; + } } diff --git a/src/Object/Event/Event.php b/src/Object/Event/Event.php new file mode 100644 index 000000000..95382cb25 --- /dev/null +++ b/src/Object/Event/Event.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\Object\Event; + +/** + * @template T of object + */ +interface Event +{ + /** + * @return class-string + */ + public function objectClassName(): string; +} diff --git a/src/Object/Event/HookListenerFilter.php b/src/Object/Event/HookListenerFilter.php new file mode 100644 index 000000000..12e28e0eb --- /dev/null +++ b/src/Object/Event/HookListenerFilter.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Object\Event; + +final class HookListenerFilter +{ + /** @var \Closure(Event): void */ + private \Closure $listener; + + /** + * @param array{0: object, 1: string} $listener + * @param class-string|null $objectClass + */ + public function __construct(array $listener, private ?string $objectClass = null) + { + if (!\is_callable($listener)) { + throw new \InvalidArgumentException(\sprintf('Listener must be a callable, "%s" given.', \get_debug_type($listener))); + } + + $this->listener = $listener(...); + } + + /** + * @param Event $event + */ + public function __invoke(Event $event): void + { + if ($this->objectClass && $event->objectClassName() !== $this->objectClass) { + return; + } + + ($this->listener)($event); + } +} diff --git a/src/Persistence/Event/AfterPersist.php b/src/Persistence/Event/AfterPersist.php index eb89763c2..6ab3fa293 100644 --- a/src/Persistence/Event/AfterPersist.php +++ b/src/Persistence/Event/AfterPersist.php @@ -14,21 +14,31 @@ namespace Zenstruck\Foundry\Persistence\Event; use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\Object\Event\Event; use Zenstruck\Foundry\Persistence\PersistentObjectFactory; /** * @author Nicolas PHILIPPE * + * @template T of object + * @implements Event + * * @phpstan-import-type Parameters from Factory */ -final class AfterPersist +final class AfterPersist implements Event { public function __construct( + /** @var T */ public readonly object $object, /** @phpstan-var Parameters */ public readonly array $parameters, - /** @var PersistentObjectFactory */ + /** @var PersistentObjectFactory */ public readonly PersistentObjectFactory $factory, ) { } + + public function objectClassName(): string + { + return $this->object::class; + } } diff --git a/src/ZenstruckFoundryBundle.php b/src/ZenstruckFoundryBundle.php index 69f4a952e..b3dd123a2 100644 --- a/src/ZenstruckFoundryBundle.php +++ b/src/ZenstruckFoundryBundle.php @@ -15,15 +15,19 @@ use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; +use Zenstruck\Foundry\Attribute\AsFoundryHook; use Zenstruck\Foundry\Attribute\AsFixture; use Zenstruck\Foundry\DependencyInjection\AsFixtureStoryCompilerPass; use Zenstruck\Foundry\InMemory\DependencyInjection\InMemoryCompilerPass; use Zenstruck\Foundry\InMemory\InMemoryRepository; use Zenstruck\Foundry\Mongo\MongoResetter; +use Zenstruck\Foundry\Object\Event\Event; +use Zenstruck\Foundry\Object\Event\HookListenerFilter; use Zenstruck\Foundry\Object\Instantiator; use Zenstruck\Foundry\ORM\ResetDatabase\MigrateDatabaseResetter; use Zenstruck\Foundry\ORM\ResetDatabase\OrmResetter; @@ -238,6 +242,25 @@ public function loadExtension(array $config, ContainerConfigurator $configurator $this->configureInMemory($configurator, $container); $this->configureFixturesStory($container); $this->configureAutoRefreshWithLazyObjects($container, $config['enable_auto_refresh_with_lazy_objects'] ?? null); + + $container->registerAttributeForAutoconfiguration( + AsFoundryHook::class, + // @phpstan-ignore argument.type + static function(ChildDefinition $definition, AsFoundryHook $attribute, \ReflectionMethod $reflector) { + if (1 !== \count($reflector->getParameters()) + || !$reflector->getParameters()[0]->getType() + || !$reflector->getParameters()[0]->getType() instanceof \ReflectionNamedType + || !\is_a($reflector->getParameters()[0]->getType()->getName(), Event::class, true) + ) { + throw new LogicException(\sprintf("In order to use \"%s\" attribute, method \"{$reflector->class}::{$reflector->name}()\" must have a single parameter that is a subclass of \"%s\".", AsFoundryHook::class, Event::class)); + } + $definition->addTag('foundry.hook', [ + 'class' => $attribute->objectClass, + 'method' => $reflector->getName(), + 'event' => $reflector->getParameters()[0]->getType()->getName(), + ]); + } + ); } public function build(ContainerBuilder $container): void @@ -258,6 +281,21 @@ public function process(ContainerBuilder $container): void ->addMethodCall('addProvider', [new Reference($id)]) ; } + + // events + $i = 0; + foreach ($container->findTaggedServiceIds('foundry.hook') as $id => $tags) { + foreach ($tags as $tag) { + $container + ->setDefinition("foundry.hook.{$tag['event']}.{$i}", new Definition(class: HookListenerFilter::class)) + ->setArgument(0, [new Reference($id), $tag['method']]) + ->setArgument(1, $tag['class']) + ->addTag('kernel.event_listener', ['event' => $tag['event']]) + ; + + ++$i; + } + } } /** diff --git a/tests/Fixture/Events/FoundryEventListener.php b/tests/Fixture/Events/FoundryEventListener.php index ddc00e4f0..c9df24006 100644 --- a/tests/Fixture/Events/FoundryEventListener.php +++ b/tests/Fixture/Events/FoundryEventListener.php @@ -14,13 +14,16 @@ namespace Zenstruck\Foundry\Tests\Fixture\Events; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Zenstruck\Foundry\Attribute\AsFoundryHook; use Zenstruck\Foundry\Object\Event\AfterInstantiate; use Zenstruck\Foundry\Object\Event\BeforeInstantiate; +use Zenstruck\Foundry\Object\Event\Event; use Zenstruck\Foundry\Persistence\Event\AfterPersist; use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForEventListeners; final class FoundryEventListener { + /** @param BeforeInstantiate $event */ #[AsEventListener] public function beforeInstantiate(BeforeInstantiate $event): void { @@ -28,9 +31,10 @@ public function beforeInstantiate(BeforeInstantiate $event): void return; } - $event->parameters['name'] = "{$event->parameters['name']}\nBeforeInstantiate"; + $event->parameters['name'] = $this->name($event->parameters['name'], $event); } + /** @param AfterInstantiate $event */ #[AsEventListener] public function afterInstantiate(AfterInstantiate $event): void { @@ -38,9 +42,10 @@ public function afterInstantiate(AfterInstantiate $event): void return; } - $event->object->name = "{$event->object->name}\nAfterInstantiate"; + $event->object->name = $this->name($event->object->name, $event); } + /** @param AfterPersist $event */ #[AsEventListener] public function afterPersist(AfterPersist $event): void { @@ -48,6 +53,67 @@ public function afterPersist(AfterPersist $event): void return; } - $event->object->name = "{$event->object->name}\nAfterPersist"; + $event->object->name = $this->name($event->object->name, $event); + } + + /** @param BeforeInstantiate $event */ + #[AsFoundryHook(EntityForEventListeners::class)] + public function beforeInstantiateWithFoundryAttribute(BeforeInstantiate $event): void + { + $event->parameters['name'] = "{$this->name($event->parameters['name'], $event)} with Foundry attribute"; + } + + /** @param AfterInstantiate $event */ + #[AsFoundryHook(EntityForEventListeners::class)] + public function afterInstantiateWithFoundryAttribute(AfterInstantiate $event): void + { + $event->object->name = "{$this->name($event->object->name, $event)} with Foundry attribute"; + } + + /** @param AfterPersist $event */ + #[AsFoundryHook(EntityForEventListeners::class)] + public function afterPersistWithFoundryAttribute(AfterPersist $event): void + { + $event->object->name = "{$this->name($event->object->name, $event)} with Foundry attribute"; + } + + /** @param BeforeInstantiate $event */ + #[AsFoundryHook()] + public function globalBeforeInstantiate(BeforeInstantiate $event): void + { + if (EntityForEventListeners::class !== $event->objectClass) { + return; + } + + $event->parameters['name'] = "{$this->name($event->parameters['name'], $event)} global"; + } + + /** @param AfterInstantiate $event */ + #[AsFoundryHook()] + public function globalAfterInstantiate(AfterInstantiate $event): void + { + if (!$event->object instanceof EntityForEventListeners) { + return; + } + + $event->object->name = "{$this->name($event->object->name, $event)} global"; + } + + /** @param AfterPersist $event */ + #[AsFoundryHook()] + public function globalAfterPersist(AfterPersist $event): void + { + if (!$event->object instanceof EntityForEventListeners) { + return; + } + + $event->object->name = "{$this->name($event->object->name, $event)} global"; + } + + private function name(string $name, Event $event): string // @phpstan-ignore missingType.generics + { + $eventName = (new \ReflectionClass($event))->getShortName(); + + return "{$name}\n{$eventName}"; } } diff --git a/tests/Integration/Persistence/EventsTest.php b/tests/Integration/EventsTest.php similarity index 75% rename from tests/Integration/Persistence/EventsTest.php rename to tests/Integration/EventsTest.php index 4989ebe5e..0f003b44c 100644 --- a/tests/Integration/Persistence/EventsTest.php +++ b/tests/Integration/EventsTest.php @@ -11,14 +11,13 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Foundry\Tests\Integration\Persistence; +namespace Zenstruck\Foundry\Tests\Integration; use PHPUnit\Framework\Attributes\Test; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Events\FactoryWithEventListeners; -use Zenstruck\Foundry\Tests\Integration\RequiresORM; final class EventsTest extends KernelTestCase { @@ -36,8 +35,14 @@ public function it_can_call_hooks(): void <<name ); From f35ccb759b9e4d863c55ecdfc6a58caea7999c05 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Wed, 24 Sep 2025 22:03:14 +0200 Subject: [PATCH 2/2] feat: use AfterPersist hook in PersistedObjectsTracker --- bin/tools/phpstan/composer.lock | 25 +++++------ config/persistence.php | 4 +- docs/index.rst | 1 + src/Attribute/AsFoundryHook.php | 2 +- src/Command/StubCommand.php | 3 ++ src/Configuration.php | 2 +- .../{Proxy => }/PersistedObjectsTracker.php | 43 +++++++++---------- src/Persistence/PersistenceManager.php | 8 ---- src/Persistence/PersistentObjectFactory.php | 12 ++---- tests/Integration/ORM/AutoRefreshTest.php | 2 +- .../Persistence/AutoRefreshTestCase.php | 2 +- 11 files changed, 48 insertions(+), 56 deletions(-) rename src/Persistence/{Proxy => }/PersistedObjectsTracker.php (62%) diff --git a/bin/tools/phpstan/composer.lock b/bin/tools/phpstan/composer.lock index 8438f788f..64e430750 100644 --- a/bin/tools/phpstan/composer.lock +++ b/bin/tools/phpstan/composer.lock @@ -122,11 +122,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.30", + "version": "2.1.31", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a4a7f159927983dd4f7c8020ed227d80b7f39d7d", - "reference": "a4a7f159927983dd4f7c8020ed227d80b7f39d7d", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ead89849d879fe203ce9292c6ef5e7e76f867b96", + "reference": "ead89849d879fe203ce9292c6ef5e7e76f867b96", "shasum": "" }, "require": { @@ -171,20 +171,20 @@ "type": "github" } ], - "time": "2025-10-02T16:07:52+00:00" + "time": "2025-10-10T14:14:11+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "2.0.6", + "version": "2.0.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "934f5734812341358fc41c44006b30fa00c785f0" + "reference": "368ad1c713a6d95763890bc2292694a603ece7c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/934f5734812341358fc41c44006b30fa00c785f0", - "reference": "934f5734812341358fc41c44006b30fa00c785f0", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/368ad1c713a6d95763890bc2292694a603ece7c8", + "reference": "368ad1c713a6d95763890bc2292694a603ece7c8", "shasum": "" }, "require": { @@ -214,11 +214,12 @@ "nesbot/carbon": "^2.49", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-deprecation-rules": "^2.0.2", - "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-phpunit": "^2.0.8", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6.20", "ramsey/uuid": "^4.2", - "symfony/cache": "^5.4" + "symfony/cache": "^5.4", + "symfony/uid": "^5.4 || ^6.4 || ^7.3" }, "type": "phpstan-extension", "extra": { @@ -241,9 +242,9 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.6" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.11" }, - "time": "2025-09-10T07:06:30+00:00" + "time": "2025-11-04T09:55:35+00:00" }, { "name": "phpstan/phpstan-phpunit", diff --git a/config/persistence.php b/config/persistence.php index c47bd9534..150332a98 100644 --- a/config/persistence.php +++ b/config/persistence.php @@ -15,8 +15,9 @@ use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; use Zenstruck\Foundry\Command\LoadFixturesCommand; +use Zenstruck\Foundry\Persistence\Event\AfterPersist; use Zenstruck\Foundry\Persistence\PersistenceManager; -use Zenstruck\Foundry\Persistence\Proxy\PersistedObjectsTracker; +use Zenstruck\Foundry\Persistence\PersistedObjectsTracker; use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager; return static function(ContainerConfigurator $container): void { @@ -47,6 +48,7 @@ ->tag('kernel.event_listener', ['event' => TerminateEvent::class, 'method' => 'refresh']) ->tag('kernel.event_listener', ['event' => ConsoleTerminateEvent::class, 'method' => 'refresh']) ->tag('kernel.event_listener', ['event' => WorkerMessageHandledEvent::class, 'method' => 'refresh']) // @phpstan-ignore class.notFound + ->tag('foundry.hook', ['class' => null, 'method' => 'afterPersistHook', 'event' => AfterPersist::class]) ; } }; diff --git a/docs/index.rst b/docs/index.rst index ea89bc211..9897437f4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -715,6 +715,7 @@ to create hooks globally: .. versionadded:: 2.8 The ``#[AsFoundryHook]`` attribute was added in Foundry 2.8. + .. note:: If you want to save data to the database in an ``AfterPersist`` listener, Foundry won't flush automatically, and you diff --git a/src/Attribute/AsFoundryHook.php b/src/Attribute/AsFoundryHook.php index 68ff14e16..15815dc3b 100644 --- a/src/Attribute/AsFoundryHook.php +++ b/src/Attribute/AsFoundryHook.php @@ -19,7 +19,7 @@ final class AsFoundryHook extends AsEventListener { public function __construct( - /** @var class-string */ + /** @var class-string|null */ public readonly ?string $objectClass = null, int $priority = 0, ) { diff --git a/src/Command/StubCommand.php b/src/Command/StubCommand.php index 0f35eecb6..134bf88b2 100644 --- a/src/Command/StubCommand.php +++ b/src/Command/StubCommand.php @@ -32,6 +32,9 @@ protected function configure(): void $this->ignoreValidationErrors(); } + /** + * @throws \RuntimeException + */ protected function execute(InputInterface $input, OutputInterface $output): int { throw new \RuntimeException($this->message); diff --git a/src/Configuration.php b/src/Configuration.php index 298826941..211ca2319 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -20,7 +20,7 @@ use Zenstruck\Foundry\InMemory\CannotEnableInMemory; use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry; use Zenstruck\Foundry\Persistence\PersistenceManager; -use Zenstruck\Foundry\Persistence\Proxy\PersistedObjectsTracker; +use Zenstruck\Foundry\Persistence\PersistedObjectsTracker; /** * @author Kevin Bond diff --git a/src/Persistence/Proxy/PersistedObjectsTracker.php b/src/Persistence/PersistedObjectsTracker.php similarity index 62% rename from src/Persistence/Proxy/PersistedObjectsTracker.php rename to src/Persistence/PersistedObjectsTracker.php index 4320a2f98..c327b80f3 100644 --- a/src/Persistence/Proxy/PersistedObjectsTracker.php +++ b/src/Persistence/PersistedObjectsTracker.php @@ -9,9 +9,10 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Foundry\Persistence\Proxy; +namespace Zenstruck\Foundry\Persistence; use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\Persistence\Event\AfterPersist; /** * @internal @@ -23,11 +24,11 @@ final class PersistedObjectsTracker * * @var \WeakMap keys: objects, values: value ids */ - private static \WeakMap $buffer; + private static \WeakMap $trackedObjects; public function __construct() { - self::$buffer ??= new \WeakMap(); + self::$trackedObjects ??= new \WeakMap(); } public function refresh(): void @@ -35,43 +36,39 @@ public function refresh(): void self::proxifyObjects(); } - public function add(object ...$objects): void + /** + * @param AfterPersist $event + */ + public function afterPersistHook(AfterPersist $event): void { - foreach ($objects as $object) { - if (self::$buffer->offsetExists($object) && self::$buffer[$object]) { - self::proxifyObject($object, self::$buffer[$object]); - - continue; - } - - self::$buffer[$object] = Configuration::instance()->persistence()->getIdentifierValues($object); + if ($event->factory instanceof PersistentProxyObjectFactory || !$event->factory->isAutorefreshEnabled()) { + return; } + + $this->add($event->object); } - public function updateIds(): void + public function add(object ...$objects): void { - foreach (self::$buffer as $object => $id) { - if (\is_array($id)) { - // in mongodb, id can be an array with null values, which is not a valid id and must be updated - $id = \array_filter($id); - } + foreach ($objects as $object) { + if (self::$trackedObjects->offsetExists($object) && self::$trackedObjects[$object]) { + self::proxifyObject($object, self::$trackedObjects[$object]); - if ($id) { continue; } - self::$buffer[$object] = Configuration::instance()->persistence()->getIdentifierValues($object); + self::$trackedObjects[$object] = Configuration::instance()->persistence()->getIdentifierValues($object); } } public static function reset(): void { - self::$buffer = new \WeakMap(); + self::$trackedObjects = new \WeakMap(); } public static function countObjects(): int { - return \count(self::$buffer); + return \count(self::$trackedObjects); } private static function proxifyObjects(): void @@ -80,7 +77,7 @@ private static function proxifyObjects(): void return; } - foreach (self::$buffer as $object => $id) { + foreach (self::$trackedObjects as $object => $id) { if (!$id) { continue; } diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index 4fb4bbf29..58c2a37c5 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -85,10 +85,6 @@ public function save(object $object): object $this->flush($om); } - if (Configuration::instance()->autoRefreshWithLazyObjectsIsEnabled()) { - Configuration::instance()->persistedObjectsTracker?->updateIds(); - } - return $object; } @@ -427,10 +423,6 @@ private function flushAllStrategies(): void $this->flush($om); } } - - if (Configuration::instance()->autoRefreshWithLazyObjectsIsEnabled()) { - Configuration::instance()->persistedObjectsTracker?->updateIds(); - } } /** diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 0ad2e1f36..42ed10d9a 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -530,13 +530,6 @@ static function(object $object, array $parameters, PersistentObjectFactory $fact return; } - if ( - $factoryUsed->isAutorefreshEnabled() - && !$factoryUsed instanceof PersistentProxyObjectFactory - ) { - Configuration::instance()->persistedObjectsTracker?->add($object); - } - $afterPersistCallbacks = []; foreach (\array_merge(...$factoryUsed->afterPersist) as $afterPersist) { @@ -567,7 +560,10 @@ static function(object $object, array $parameters, self $factoryUsed): bool { ); } - private function isAutorefreshEnabled(): bool + /** + * @internal + */ + public function isAutorefreshEnabled(): bool { return $this->autorefreshEnabled ??= Configuration::autoRefreshWithLazyObjectsIsEnabled(); } diff --git a/tests/Integration/ORM/AutoRefreshTest.php b/tests/Integration/ORM/AutoRefreshTest.php index 4e141502e..05bddc2dc 100644 --- a/tests/Integration/ORM/AutoRefreshTest.php +++ b/tests/Integration/ORM/AutoRefreshTest.php @@ -23,7 +23,7 @@ use PHPUnit\Framework\Attributes\Test; use Zenstruck\Foundry\Configuration; use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Persistence\Proxy\PersistedObjectsTracker; +use Zenstruck\Foundry\Persistence\PersistedObjectsTracker; use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangesEntityRelationshipCascadePersist; use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships; use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; diff --git a/tests/Integration/Persistence/AutoRefreshTestCase.php b/tests/Integration/Persistence/AutoRefreshTestCase.php index 96b39c54a..0e161539c 100644 --- a/tests/Integration/Persistence/AutoRefreshTestCase.php +++ b/tests/Integration/Persistence/AutoRefreshTestCase.php @@ -25,7 +25,7 @@ use Symfony\Component\HttpKernel\KernelInterface; use Zenstruck\Foundry\Configuration; use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Persistence\Proxy\PersistedObjectsTracker; +use Zenstruck\Foundry\Persistence\PersistedObjectsTracker; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Document\DocumentWithReadonly;