Skip to content

Commit c21a69d

Browse files
committed
feat: dispatch global events
1 parent fe117df commit c21a69d

17 files changed

+380
-3
lines changed

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"symfony/browser-kit": "^6.4|^7.0|^8.0",
4242
"symfony/console": "^6.4|^7.0|^8.0",
4343
"symfony/dotenv": "^6.4|^7.0|^8.0",
44+
"symfony/event-dispatcher": "^6.4|^7.0",
4445
"symfony/framework-bundle": "^6.4|^7.0|^8.0",
4546
"symfony/maker-bundle": "^1.55",
4647
"symfony/phpunit-bridge": "^6.4|^7.0|^8.0",
@@ -84,6 +85,7 @@
8485
},
8586
"conflict": {
8687
"doctrine/persistence": "<2.0",
88+
"symfony/event-dispatcher": "<6.4",
8789
"symfony/framework-bundle": "<6.4"
8890
},
8991
"extra": {

config/services.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
service('.zenstruck_foundry.in_memory.repository_registry'),
3838
service('.foundry.persistence.objects_tracker')->nullOnInvalid(),
3939
param('zenstruck_foundry.enable_auto_refresh_with_lazy_objects'),
40+
service('event_dispatcher')->nullOnInvalid(),
4041
])
4142
->public()
4243
;

docs/index.rst

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,52 @@ You can also add hooks directly in your factory class:
644644

645645
Read `Initialization`_ to learn more about the ``initialize()`` method.
646646

647+
Events
648+
~~~~~~
649+
650+
In addition to hooks, Foundry also leverages `symfony/event-dispatcher` and dispatches events that you can listen to,
651+
allowing to create hooks globally, as Symfony services:
652+
653+
::
654+
655+
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
656+
use Zenstruck\Foundry\Object\Event\AfterInstantiate;
657+
use Zenstruck\Foundry\Object\Event\BeforeInstantiate;
658+
use Zenstruck\Foundry\Persistence\Event\AfterPersist;
659+
660+
final class FoundryEventListener
661+
{
662+
#[AsEventListener]
663+
public function beforeInstantiate(BeforeInstantiate $event): void
664+
{
665+
// do something before the object is instantiated:
666+
// $event->parameters is what will be used to instantiate the object, manipulate as required
667+
// $event->objectClass is the class of the object being instantiated
668+
// $event->factory is the factory instance which creates the object
669+
}
670+
671+
#[AsEventListener]
672+
public function afterInstantiate(AfterInstantiate $event): void
673+
{
674+
// $event->object is the instantiated object
675+
// $event->parameters contains the attributes used to instantiate the object and any extras
676+
// $event->factory is the factory instance which creates the object
677+
}
678+
679+
#[AsEventListener]
680+
public function afterPersist(AfterPersist $event): void
681+
{
682+
// this event is only called if the object was persisted
683+
// $event->object is the persisted Post object
684+
// $event->parameters contains the attributes used to instantiate the object and any extras
685+
// $event->factory is the factory instance which creates the object
686+
}
687+
}
688+
689+
.. versionadded:: 2.4
690+
691+
Those events are triggered since Foundry 2.4.
692+
647693
Initialization
648694
~~~~~~~~~~~~~~
649695

src/Configuration.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Zenstruck\Foundry;
1313

1414
use Faker;
15+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1516
use Zenstruck\Foundry\Exception\FactoriesTraitNotUsed;
1617
use Zenstruck\Foundry\Exception\FoundryNotBooted;
1718
use Zenstruck\Foundry\Exception\PersistenceDisabled;
@@ -63,6 +64,7 @@ public function __construct(
6364
public readonly ?InMemoryRepositoryRegistry $inMemoryRepositoryRegistry = null,
6465
public readonly ?PersistedObjectsTracker $persistedObjectsTracker = null,
6566
private readonly bool $enableAutoRefreshWithLazyObjects = false,
67+
private readonly ?EventDispatcherInterface $eventDispatcher = null,
6668
) {
6769
if (null === self::$instance) {
6870
$this->faker->seed(self::fakerSeed($forcedFakerSeed));
@@ -106,6 +108,16 @@ public function assertPersistenceEnabled(): void
106108
}
107109
}
108110

111+
public function hasEventDispatcher(): bool
112+
{
113+
return (bool) $this->eventDispatcher;
114+
}
115+
116+
public function eventDispatcher(): EventDispatcherInterface
117+
{
118+
return $this->eventDispatcher ?? throw new \RuntimeException('No event dispatcher configured.');
119+
}
120+
109121
public function inADataProvider(): bool
110122
{
111123
return $this->bootedForDataProvider;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the zenstruck/foundry package.
7+
*
8+
* (c) Kevin Bond <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zenstruck\Foundry\Object\Event;
15+
16+
use Zenstruck\Foundry\Factory;
17+
use Zenstruck\Foundry\ObjectFactory;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <[email protected]>
21+
*
22+
* @phpstan-import-type Parameters from Factory
23+
*/
24+
final class AfterInstantiate
25+
{
26+
public function __construct(
27+
public readonly object $object,
28+
/** @phpstan-var Parameters */
29+
public readonly array $parameters,
30+
/** @var ObjectFactory<object> */
31+
public readonly ObjectFactory $factory,
32+
) {
33+
}
34+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the zenstruck/foundry package.
7+
*
8+
* (c) Kevin Bond <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zenstruck\Foundry\Object\Event;
15+
16+
use Zenstruck\Foundry\Factory;
17+
use Zenstruck\Foundry\ObjectFactory;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <[email protected]>
21+
*
22+
* @phpstan-import-type Parameters from Factory
23+
*/
24+
final class BeforeInstantiate
25+
{
26+
public function __construct(
27+
/** @phpstan-var Parameters */
28+
public array $parameters,
29+
/** @var class-string */
30+
public readonly string $objectClass,
31+
/** @var ObjectFactory<object> */
32+
public readonly ObjectFactory $factory,
33+
) {
34+
}
35+
}

src/ObjectFactory.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Zenstruck\Foundry;
1313

14+
use Zenstruck\Foundry\Object\Event\AfterInstantiate;
15+
use Zenstruck\Foundry\Object\Event\BeforeInstantiate;
1416
use Zenstruck\Foundry\Object\Instantiator;
1517
use Zenstruck\Foundry\Persistence\ProxyGenerator;
1618

@@ -191,6 +193,33 @@ final protected function normalizeReusedAttributes(): array
191193
return $attributes;
192194
}
193195

196+
/**
197+
* @internal
198+
*/
199+
protected function initializeInternal(): static
200+
{
201+
if (!Configuration::isBooted() || !Configuration::instance()->hasEventDispatcher()) {
202+
return $this;
203+
}
204+
205+
return $this->beforeInstantiate(
206+
static function(array $parameters, string $objectClass, self $usedFactory): array {
207+
Configuration::instance()->eventDispatcher()->dispatch(
208+
$hook = new BeforeInstantiate($parameters, $objectClass, $usedFactory)
209+
);
210+
211+
return $hook->parameters;
212+
}
213+
)
214+
->afterInstantiate(
215+
static function(object $object, array $parameters, self $usedFactory): void {
216+
Configuration::instance()->eventDispatcher()->dispatch(
217+
new AfterInstantiate($object, $parameters, $usedFactory)
218+
);
219+
}
220+
);
221+
}
222+
194223
/**
195224
* @return list<object>
196225
* @internal
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the zenstruck/foundry package.
7+
*
8+
* (c) Kevin Bond <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zenstruck\Foundry\Persistence\Event;
15+
16+
use Zenstruck\Foundry\Factory;
17+
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <[email protected]>
21+
*
22+
* @phpstan-import-type Parameters from Factory
23+
*/
24+
final class AfterPersist
25+
{
26+
public function __construct(
27+
public readonly object $object,
28+
/** @phpstan-var Parameters */
29+
public readonly array $parameters,
30+
/** @var PersistentObjectFactory<object> */
31+
public readonly PersistentObjectFactory $factory,
32+
) {
33+
}
34+
}

src/Persistence/PersistentObjectFactory.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Zenstruck\Foundry\FactoryCollection;
2222
use Zenstruck\Foundry\Object\Hydrator;
2323
use Zenstruck\Foundry\ObjectFactory;
24+
use Zenstruck\Foundry\Persistence\Event\AfterPersist;
2425
use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects;
2526
use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed;
2627
use Zenstruck\Foundry\Persistence\Relationship\ManyToOneRelationship;
@@ -514,10 +515,13 @@ protected function normalizeObject(string $field, object $object): object
514515
}
515516
}
516517

518+
/**
519+
* @internal
520+
*/
517521
final protected function initializeInternal(): static
518522
{
519523
// Schedule any new object for insert right after instantiation
520-
return parent::initializeInternal()
524+
$factory = parent::initializeInternal()
521525
->afterInstantiate(
522526
static function(object $object, array $parameters, PersistentObjectFactory $factoryUsed): void {
523527
if (!$factoryUsed->isPersisting()) {
@@ -544,6 +548,20 @@ static function(object $object, array $parameters, PersistentObjectFactory $fact
544548
}
545549
)
546550
;
551+
552+
if (!Configuration::isBooted() || !Configuration::instance()->hasEventDispatcher()) {
553+
return $factory;
554+
}
555+
556+
return $factory->afterPersist(
557+
static function(object $object, array $parameters, self $factoryUsed): bool {
558+
Configuration::instance()->eventDispatcher()->dispatch(
559+
new AfterPersist($object, $parameters, $factoryUsed)
560+
);
561+
562+
return false; // don't perform a flush after the hook
563+
}
564+
);
547565
}
548566

549567
private function throwIfCannotCreateObject(): void

tests/Benchmark/KernelBench.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ protected static function createKernel(array $options = []): KernelInterface
148148
$env = $options['environment'] ?? $_ENV['APP_ENV'] ?? $_SERVER['APP_ENV'] ?? 'test';
149149
$debug = $options['debug'] ?? $_ENV['APP_DEBUG'] ?? $_SERVER['APP_DEBUG'] ?? true;
150150

151-
return new static::$class($env, $debug);
151+
return new static::$class($env, false);
152152
}
153153

154154
/**

0 commit comments

Comments
 (0)