diff --git a/.roave-backward-compatibility-check.xml b/.roave-backward-compatibility-check.xml
index fb117d213..507ce5eb6 100644
--- a/.roave-backward-compatibility-check.xml
+++ b/.roave-backward-compatibility-check.xml
@@ -7,5 +7,6 @@
#\[BC\] SKIPPED: Roave\\BetterReflection\\Reflection\\ReflectionClass "Psalm\\Plugin\\PluginEntryPointInterface" could not be found in the located source##\[BC\] SKIPPED: Roave\\BetterReflection\\Reflection\\ReflectionClass "Psalm\\Plugin\\EventHandler\\AfterMethodCallAnalysisInterface" could not be found in the located source##(.*)Zenstruck\\Foundry\\Utils\\Rector(.*)#
+ #(.*)initializeInternal(.*)#
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 acc4a128d..05e9e5849 100644
--- a/config/services.php
+++ b/config/services.php
@@ -46,6 +46,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 a4befee73..84dc86953 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -659,6 +659,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 818941d30..298826941 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