From fe117df987ed97d4c04b651c4e6af47ddc956014 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Wed, 24 Sep 2025 18:31:09 +0200 Subject: [PATCH 1/4] feat: after persist callback choces if a flush is perfromed afterwards --- src/Persistence/PersistenceManager.php | 16 ++++-- src/Persistence/PersistentObjectFactory.php | 9 +-- .../ORM/GenericEntityFactoryTest.php | 55 +++++++++++++++++++ .../Persistence/GenericFactoryTestCase.php | 1 + 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index bd2d611ff..e7f36dda4 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -36,7 +36,7 @@ final class PersistenceManager private bool $flush = true; private bool $persist = true; - /** @var list */ + /** @var list */ private array $afterPersistCallbacks = []; /** @@ -80,9 +80,9 @@ public function save(object $object): object $om->persist($object); $this->flush($om); - $callbacksCalled = $this->callPostPersistCallbacks(); + $shouldFlush = $this->callPostPersistCallbacks(); - if ($callbacksCalled) { + if ($shouldFlush) { $this->flush($om); } @@ -95,7 +95,7 @@ public function save(object $object): object * @template T of object * * @param T $object - * @param list $afterPersistCallbacks + * @param list $afterPersistCallbacks * * @return T */ @@ -457,11 +457,15 @@ private function callPostPersistCallbacks(): bool $afterPersistCallbacks = $this->afterPersistCallbacks; $this->afterPersistCallbacks = []; + $shouldFlush = false; + foreach ($afterPersistCallbacks as $afterPersistCallback) { - $afterPersistCallback(); + if ($afterPersistCallback()) { + $shouldFlush = true; + } } - return true; + return $shouldFlush; } /** diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 411e5a268..d7b073134 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -43,7 +43,7 @@ abstract class PersistentObjectFactory extends ObjectFactory { private PersistMode $persist = PersistMode::PERSIST; - /** @phpstan-var list */ + /** @phpstan-var list */ private array $afterPersist = []; /** @var list */ @@ -327,7 +327,7 @@ public function withPersistMode(PersistMode $persistMode): static } /** - * @phpstan-param callable(T, Parameters, static):void $callback + * @phpstan-param callable(T, Parameters, static):void|callable(T, Parameters, static):bool $callback return value tells if a flush should be performed after the callback */ final public function afterPersist(callable $callback): static { @@ -534,8 +534,9 @@ static function(object $object, array $parameters, PersistentObjectFactory $fact $afterPersistCallbacks = []; foreach ($factoryUsed->afterPersist as $afterPersist) { - $afterPersistCallbacks[] = static function() use ($object, $afterPersist, $parameters, $factoryUsed): void { - $afterPersist($object, $parameters, $factoryUsed); + $afterPersistCallbacks[] = static function() use ($object, $afterPersist, $parameters, $factoryUsed): bool { + // this condition is needed to avoid BC breaks: only avoir flush if the callback returns false + return !($afterPersist($object, $parameters, $factoryUsed) === false); }; } diff --git a/tests/Integration/ORM/GenericEntityFactoryTest.php b/tests/Integration/ORM/GenericEntityFactoryTest.php index 170b3cd85..972cfe765 100644 --- a/tests/Integration/ORM/GenericEntityFactoryTest.php +++ b/tests/Integration/ORM/GenericEntityFactoryTest.php @@ -11,9 +11,11 @@ namespace Zenstruck\Foundry\Tests\Integration\ORM; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\EmptyConstructorFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; use Zenstruck\Foundry\Tests\Integration\Persistence\GenericFactoryTestCase; use Zenstruck\Foundry\Tests\Integration\RequiresORM; @@ -55,6 +57,59 @@ public function can_use_factory_with_empty_constructor_without_persistence(): vo EmptyConstructorFactory::assert()->count(0); } + /** + * @test + * + * @phpstan-ignore missingType.callable + */ + #[Test] + #[DataProvider('afterPersistDecideFlushProvider')] + public function after_persist_callback_can_decide_if_flush_is_performed_afterwards(callable $callback, string $expected): void + { + static::factory() + ->afterPersist($callback) + ->create(['prop1' => 'foo']); + + static::factory()::assert()->exists(['prop1' => $expected]); + } + + public static function afterPersistDecideFlushProvider(): iterable + { + yield 'no return will flush' => [ + function(GenericModel $object) { + $object->setProp1('bar'); + }, + 'bar' + ]; + + yield 'return true will flush' => [ + function(GenericModel $object) { + $object->setProp1('bar'); + + return true; + }, + 'bar' + ]; + + yield 'return something else than false will flush' => [ + function(GenericModel $object) { + $object->setProp1('bar'); + + return $object; + }, + 'bar' + ]; + + yield 'return false will not flush' => [ + function(GenericModel $object) { + $object->setProp1('bar'); + + return false; + }, + 'foo' + ]; + } + protected static function factory(): GenericEntityFactory { return GenericEntityFactory::new(); diff --git a/tests/Integration/Persistence/GenericFactoryTestCase.php b/tests/Integration/Persistence/GenericFactoryTestCase.php index 1f5786835..50baec1db 100644 --- a/tests/Integration/Persistence/GenericFactoryTestCase.php +++ b/tests/Integration/Persistence/GenericFactoryTestCase.php @@ -11,6 +11,7 @@ namespace Zenstruck\Foundry\Tests\Integration\Persistence; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Depends; use PHPUnit\Framework\Attributes\Test; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; From c21a69dff22396c7df7b926bfd53b20fc474c8da Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Wed, 24 Sep 2025 18:32:02 +0200 Subject: [PATCH 2/4] feat: dispatch global events --- composer.json | 2 + config/services.php | 1 + docs/index.rst | 46 ++++++++++++++++ src/Configuration.php | 12 +++++ src/Object/Event/AfterInstantiate.php | 34 ++++++++++++ src/Object/Event/BeforeInstantiate.php | 35 ++++++++++++ src/ObjectFactory.php | 29 ++++++++++ src/Persistence/Event/AfterPersist.php | 34 ++++++++++++ src/Persistence/PersistentObjectFactory.php | 20 ++++++- tests/Benchmark/KernelBench.php | 2 +- .../Entity/EntityForEventListeners.php | 27 ++++++++++ .../Events/FactoryWithEventListeners.php | 38 +++++++++++++ tests/Fixture/Events/FoundryEventListener.php | 53 +++++++++++++++++++ tests/Fixture/TestKernel.php | 3 ++ .../ORM/GenericEntityFactoryTest.php | 1 + tests/Integration/Persistence/EventsTest.php | 45 ++++++++++++++++ .../Persistence/GenericFactoryTestCase.php | 1 - 17 files changed, 380 insertions(+), 3 deletions(-) create mode 100644 src/Object/Event/AfterInstantiate.php create mode 100644 src/Object/Event/BeforeInstantiate.php create mode 100644 src/Persistence/Event/AfterPersist.php create mode 100644 tests/Fixture/Entity/EntityForEventListeners.php create mode 100644 tests/Fixture/Events/FactoryWithEventListeners.php create mode 100644 tests/Fixture/Events/FoundryEventListener.php create mode 100644 tests/Integration/Persistence/EventsTest.php diff --git a/composer.json b/composer.json index 51901d40f..aa9e2bb58 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,7 @@ "symfony/browser-kit": "^6.4|^7.0|^8.0", "symfony/console": "^6.4|^7.0|^8.0", "symfony/dotenv": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0", "symfony/framework-bundle": "^6.4|^7.0|^8.0", "symfony/maker-bundle": "^1.55", "symfony/phpunit-bridge": "^6.4|^7.0|^8.0", @@ -84,6 +85,7 @@ }, "conflict": { "doctrine/persistence": "<2.0", + "symfony/event-dispatcher": "<6.4", "symfony/framework-bundle": "<6.4" }, "extra": { diff --git a/config/services.php b/config/services.php index 70df22739..e11e0a2f3 100644 --- a/config/services.php +++ b/config/services.php @@ -37,6 +37,7 @@ service('.zenstruck_foundry.in_memory.repository_registry'), service('.foundry.persistence.objects_tracker')->nullOnInvalid(), param('zenstruck_foundry.enable_auto_refresh_with_lazy_objects'), + service('event_dispatcher')->nullOnInvalid(), ]) ->public() ; diff --git a/docs/index.rst b/docs/index.rst index 1355a89c6..a2a7e7dc0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -644,6 +644,52 @@ You can also add hooks directly in your factory class: Read `Initialization`_ to learn more about the ``initialize()`` method. +Events +~~~~~~ + +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: + +:: + + use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + use Zenstruck\Foundry\Object\Event\AfterInstantiate; + use Zenstruck\Foundry\Object\Event\BeforeInstantiate; + use Zenstruck\Foundry\Persistence\Event\AfterPersist; + + final class FoundryEventListener + { + #[AsEventListener] + public function beforeInstantiate(BeforeInstantiate $event): void + { + // do something before the object 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] + public function afterInstantiate(AfterInstantiate $event): void + { + // $event->object is the instantiated 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] + public function afterPersist(AfterPersist $event): void + { + // this event is only called if the object was persisted + // $event->object is the persisted 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 + } + } + +.. versionadded:: 2.4 + + Those events are triggered since Foundry 2.4. + Initialization ~~~~~~~~~~~~~~ diff --git a/src/Configuration.php b/src/Configuration.php index d531f27cd..3f3562bbb 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -12,6 +12,7 @@ namespace Zenstruck\Foundry; use Faker; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Zenstruck\Foundry\Exception\FactoriesTraitNotUsed; use Zenstruck\Foundry\Exception\FoundryNotBooted; use Zenstruck\Foundry\Exception\PersistenceDisabled; @@ -63,6 +64,7 @@ public function __construct( public readonly ?InMemoryRepositoryRegistry $inMemoryRepositoryRegistry = null, public readonly ?PersistedObjectsTracker $persistedObjectsTracker = null, private readonly bool $enableAutoRefreshWithLazyObjects = false, + private readonly ?EventDispatcherInterface $eventDispatcher = null, ) { if (null === self::$instance) { $this->faker->seed(self::fakerSeed($forcedFakerSeed)); @@ -106,6 +108,16 @@ public function assertPersistenceEnabled(): void } } + public function hasEventDispatcher(): bool + { + return (bool) $this->eventDispatcher; + } + + public function eventDispatcher(): EventDispatcherInterface + { + return $this->eventDispatcher ?? throw new \RuntimeException('No event dispatcher configured.'); + } + public function inADataProvider(): bool { return $this->bootedForDataProvider; diff --git a/src/Object/Event/AfterInstantiate.php b/src/Object/Event/AfterInstantiate.php new file mode 100644 index 000000000..7bbef74f5 --- /dev/null +++ b/src/Object/Event/AfterInstantiate.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\Object\Event; + +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\ObjectFactory; + +/** + * @author Nicolas PHILIPPE + * + * @phpstan-import-type Parameters from Factory + */ +final class AfterInstantiate +{ + public function __construct( + public readonly object $object, + /** @phpstan-var Parameters */ + public readonly array $parameters, + /** @var ObjectFactory */ + public readonly ObjectFactory $factory, + ) { + } +} diff --git a/src/Object/Event/BeforeInstantiate.php b/src/Object/Event/BeforeInstantiate.php new file mode 100644 index 000000000..b5ad5cb38 --- /dev/null +++ b/src/Object/Event/BeforeInstantiate.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Object\Event; + +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\ObjectFactory; + +/** + * @author Nicolas PHILIPPE + * + * @phpstan-import-type Parameters from Factory + */ +final class BeforeInstantiate +{ + public function __construct( + /** @phpstan-var Parameters */ + public array $parameters, + /** @var class-string */ + public readonly string $objectClass, + /** @var ObjectFactory */ + public readonly ObjectFactory $factory, + ) { + } +} diff --git a/src/ObjectFactory.php b/src/ObjectFactory.php index 70e4a12ac..c61875f9c 100644 --- a/src/ObjectFactory.php +++ b/src/ObjectFactory.php @@ -11,6 +11,8 @@ namespace Zenstruck\Foundry; +use Zenstruck\Foundry\Object\Event\AfterInstantiate; +use Zenstruck\Foundry\Object\Event\BeforeInstantiate; use Zenstruck\Foundry\Object\Instantiator; use Zenstruck\Foundry\Persistence\ProxyGenerator; @@ -191,6 +193,33 @@ final protected function normalizeReusedAttributes(): array return $attributes; } + /** + * @internal + */ + protected function initializeInternal(): static + { + if (!Configuration::isBooted() || !Configuration::instance()->hasEventDispatcher()) { + return $this; + } + + return $this->beforeInstantiate( + static function(array $parameters, string $objectClass, self $usedFactory): array { + Configuration::instance()->eventDispatcher()->dispatch( + $hook = new BeforeInstantiate($parameters, $objectClass, $usedFactory) + ); + + return $hook->parameters; + } + ) + ->afterInstantiate( + static function(object $object, array $parameters, self $usedFactory): void { + Configuration::instance()->eventDispatcher()->dispatch( + new AfterInstantiate($object, $parameters, $usedFactory) + ); + } + ); + } + /** * @return list * @internal diff --git a/src/Persistence/Event/AfterPersist.php b/src/Persistence/Event/AfterPersist.php new file mode 100644 index 000000000..eb89763c2 --- /dev/null +++ b/src/Persistence/Event/AfterPersist.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\Persistence\Event; + +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; + +/** + * @author Nicolas PHILIPPE + * + * @phpstan-import-type Parameters from Factory + */ +final class AfterPersist +{ + public function __construct( + public readonly object $object, + /** @phpstan-var Parameters */ + public readonly array $parameters, + /** @var PersistentObjectFactory */ + public readonly PersistentObjectFactory $factory, + ) { + } +} diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index d7b073134..a711e6630 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -21,6 +21,7 @@ use Zenstruck\Foundry\FactoryCollection; use Zenstruck\Foundry\Object\Hydrator; use Zenstruck\Foundry\ObjectFactory; +use Zenstruck\Foundry\Persistence\Event\AfterPersist; use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects; use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; use Zenstruck\Foundry\Persistence\Relationship\ManyToOneRelationship; @@ -514,10 +515,13 @@ protected function normalizeObject(string $field, object $object): object } } + /** + * @internal + */ final protected function initializeInternal(): static { // Schedule any new object for insert right after instantiation - return parent::initializeInternal() + $factory = parent::initializeInternal() ->afterInstantiate( static function(object $object, array $parameters, PersistentObjectFactory $factoryUsed): void { if (!$factoryUsed->isPersisting()) { @@ -544,6 +548,20 @@ static function(object $object, array $parameters, PersistentObjectFactory $fact } ) ; + + if (!Configuration::isBooted() || !Configuration::instance()->hasEventDispatcher()) { + return $factory; + } + + return $factory->afterPersist( + static function(object $object, array $parameters, self $factoryUsed): bool { + Configuration::instance()->eventDispatcher()->dispatch( + new AfterPersist($object, $parameters, $factoryUsed) + ); + + return false; // don't perform a flush after the hook + } + ); } private function throwIfCannotCreateObject(): void diff --git a/tests/Benchmark/KernelBench.php b/tests/Benchmark/KernelBench.php index 5a696a41f..56dcad595 100644 --- a/tests/Benchmark/KernelBench.php +++ b/tests/Benchmark/KernelBench.php @@ -148,7 +148,7 @@ protected static function createKernel(array $options = []): KernelInterface $env = $options['environment'] ?? $_ENV['APP_ENV'] ?? $_SERVER['APP_ENV'] ?? 'test'; $debug = $options['debug'] ?? $_ENV['APP_DEBUG'] ?? $_SERVER['APP_DEBUG'] ?? true; - return new static::$class($env, $debug); + return new static::$class($env, false); } /** diff --git a/tests/Fixture/Entity/EntityForEventListeners.php b/tests/Fixture/Entity/EntityForEventListeners.php new file mode 100644 index 000000000..4d36182b5 --- /dev/null +++ b/tests/Fixture/Entity/EntityForEventListeners.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 EntityForEventListeners extends Base +{ + public function __construct( + #[ORM\Column()] + public string $name, + ) { + } +} diff --git a/tests/Fixture/Events/FactoryWithEventListeners.php b/tests/Fixture/Events/FactoryWithEventListeners.php new file mode 100644 index 000000000..dcda63c7b --- /dev/null +++ b/tests/Fixture/Events/FactoryWithEventListeners.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\Events; + +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForEventListeners; + +/** + * @extends PersistentObjectFactory + */ +final class FactoryWithEventListeners extends PersistentObjectFactory +{ + public static function class(): string + { + return EntityForEventListeners::class; + } + + /** + * @return array + */ + protected function defaults(): array + { + return [ + 'name' => self::faker()->sentence(), + ]; + } +} diff --git a/tests/Fixture/Events/FoundryEventListener.php b/tests/Fixture/Events/FoundryEventListener.php new file mode 100644 index 000000000..ddc00e4f0 --- /dev/null +++ b/tests/Fixture/Events/FoundryEventListener.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Events; + +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Zenstruck\Foundry\Object\Event\AfterInstantiate; +use Zenstruck\Foundry\Object\Event\BeforeInstantiate; +use Zenstruck\Foundry\Persistence\Event\AfterPersist; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForEventListeners; + +final class FoundryEventListener +{ + #[AsEventListener] + public function beforeInstantiate(BeforeInstantiate $event): void + { + if (EntityForEventListeners::class !== $event->objectClass) { + return; + } + + $event->parameters['name'] = "{$event->parameters['name']}\nBeforeInstantiate"; + } + + #[AsEventListener] + public function afterInstantiate(AfterInstantiate $event): void + { + if (!$event->object instanceof EntityForEventListeners) { + return; + } + + $event->object->name = "{$event->object->name}\nAfterInstantiate"; + } + + #[AsEventListener] + public function afterPersist(AfterPersist $event): void + { + if (!$event->object instanceof EntityForEventListeners) { + return; + } + + $event->object->name = "{$event->object->name}\nAfterPersist"; + } +} diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index adf799e8a..9884ad75f 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -19,6 +19,7 @@ use Zenstruck\Foundry\Tests\Fixture\App\Command\UpdateGenericModelCommand; use Zenstruck\Foundry\Tests\Fixture\App\Controller\DeleteGenericModel; use Zenstruck\Foundry\Tests\Fixture\App\Controller\UpdateGenericModel; +use Zenstruck\Foundry\Tests\Fixture\Events\FoundryEventListener; use Zenstruck\Foundry\Tests\Fixture\Factories\ArrayFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryAddressRepository; @@ -61,6 +62,8 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load $c->register(InMemoryAddressRepository::class)->setAutowired(true)->setAutoconfigured(true); $c->register(InMemoryContactRepository::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(FoundryEventListener::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(DeleteGenericModel::class)->setAutowired(true)->setAutoconfigured(true)->addTag('controller.service_arguments'); $c->register(UpdateGenericModel::class)->setAutowired(true)->setAutoconfigured(true)->addTag('controller.service_arguments'); $c->register(UpdateGenericModelCommand::class)->setAutowired(true)->setAutoconfigured(true); diff --git a/tests/Integration/ORM/GenericEntityFactoryTest.php b/tests/Integration/ORM/GenericEntityFactoryTest.php index 972cfe765..23224f8b4 100644 --- a/tests/Integration/ORM/GenericEntityFactoryTest.php +++ b/tests/Integration/ORM/GenericEntityFactoryTest.php @@ -59,6 +59,7 @@ public function can_use_factory_with_empty_constructor_without_persistence(): vo /** * @test + * @dataProvider afterPersistDecideFlushProvider * * @phpstan-ignore missingType.callable */ diff --git a/tests/Integration/Persistence/EventsTest.php b/tests/Integration/Persistence/EventsTest.php new file mode 100644 index 000000000..4989ebe5e --- /dev/null +++ b/tests/Integration/Persistence/EventsTest.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\Tests\Integration\Persistence; + +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 +{ + use Factories, RequiresORM, ResetDatabase; + + /** + * @test + */ + #[Test] + public function it_can_call_hooks(): void + { + $address = FactoryWithEventListeners::createOne(['name' => 'events']); + + self::assertSame( + <<name + ); + } +} diff --git a/tests/Integration/Persistence/GenericFactoryTestCase.php b/tests/Integration/Persistence/GenericFactoryTestCase.php index 50baec1db..1f5786835 100644 --- a/tests/Integration/Persistence/GenericFactoryTestCase.php +++ b/tests/Integration/Persistence/GenericFactoryTestCase.php @@ -11,7 +11,6 @@ namespace Zenstruck\Foundry\Tests\Integration\Persistence; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Depends; use PHPUnit\Framework\Attributes\Test; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; From 6f0365f94de9429f2a0fcf718653868647cbe59e Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Wed, 24 Sep 2025 21:36:12 +0200 Subject: [PATCH 3/4] feat: introduce attribute #[AsFoudryHook] --- docs/index.rst | 28 +++++--- 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(+), 23 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 a2a7e7dc0..0965b1023 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -644,11 +644,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: :: @@ -657,26 +657,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 @@ -684,11 +684,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.4 - Those events are triggered since Foundry 2.4. + The ``#[AsFoundryHook]`` attribute was added in Foundry 2.4. Initialization ~~~~~~~~~~~~~~ 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 541884a13..2ab2e0e5d 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 f9294dd18caac910ee3eb0bfe823ae4334618f72 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Wed, 24 Sep 2025 22:03:14 +0200 Subject: [PATCH 4/4] feat: use AfterPersist hook in PersistedObjectsTracker --- config/persistence.php | 4 ++- src/Configuration.php | 2 +- .../{Proxy => }/PersistedObjectsTracker.php | 36 ++++++++++--------- src/Persistence/PersistenceManager.php | 3 -- src/Persistence/PersistentObjectFactory.php | 12 +++---- tests/Integration/ORM/AutoRefreshTest.php | 2 +- .../Persistence/AutoRefreshTestCase.php | 2 +- 7 files changed, 30 insertions(+), 31 deletions(-) rename src/Persistence/{Proxy => }/PersistedObjectsTracker.php (64%) diff --git a/config/persistence.php b/config/persistence.php index ccc3798e7..e8a79dd45 100644 --- a/config/persistence.php +++ b/config/persistence.php @@ -6,8 +6,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 { @@ -38,6 +39,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/src/Configuration.php b/src/Configuration.php index 3f3562bbb..14c9e5c2a 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 64% rename from src/Persistence/Proxy/PersistedObjectsTracker.php rename to src/Persistence/PersistedObjectsTracker.php index 007789b06..bcae6abf4 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,36 +36,37 @@ 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]) { - continue; - } - - self::$buffer[$object] = Configuration::instance()->persistence()->getIdentifierValues($object); + if ($event->factory instanceof PersistentProxyObjectFactory || !$event->factory->autorefreshEnabled()) { + return; } + + $this->add($event->object); } - public static function updateIds(): void + public function add(object ...$objects): void { - foreach (self::$buffer as $object => $id) { - if ($id) { + foreach ($objects as $object) { + if (self::$trackedObjects->offsetExists($object) && self::$trackedObjects[$object]) { 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 @@ -73,7 +75,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 e7f36dda4..2693390bb 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -22,7 +22,6 @@ use Zenstruck\Foundry\Persistence\Exception\ObjectHasUnsavedChanges; use Zenstruck\Foundry\Persistence\Exception\ObjectNoLongerExist; use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; -use Zenstruck\Foundry\Persistence\Proxy\PersistedObjectsTracker; use Zenstruck\Foundry\Persistence\Relationship\RelationshipMetadata; use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager; @@ -86,8 +85,6 @@ public function save(object $object): object $this->flush($om); } - PersistedObjectsTracker::updateIds(); - return $object; } diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index a711e6630..b2f576f5f 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -316,6 +316,11 @@ final public function withoutAutorefresh(): static return $clone; } + final public function autorefreshEnabled(): bool + { + return $this->autorefreshEnabled; + } + /** * @internal */ @@ -528,13 +533,6 @@ static function(object $object, array $parameters, PersistentObjectFactory $fact return; } - if ( - $factoryUsed->autorefreshEnabled - && !$factoryUsed instanceof PersistentProxyObjectFactory - ) { - Configuration::instance()->persistedObjectsTracker?->add($object); - } - $afterPersistCallbacks = []; foreach ($factoryUsed->afterPersist as $afterPersist) { diff --git a/tests/Integration/ORM/AutoRefreshTest.php b/tests/Integration/ORM/AutoRefreshTest.php index 19c97b4b5..50fb0f336 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 6763b1107..dd62a7909 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\Model\GenericModel;