Skip to content

Commit

Permalink
feat: dispatch events (#790)
Browse files Browse the repository at this point in the history
  • Loading branch information
nikophil authored Feb 2, 2025
1 parent 5681bb9 commit 34101a7
Show file tree
Hide file tree
Showing 14 changed files with 390 additions and 22 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"doctrine/persistence": "^2.0|^3.0",
"fakerphp/faker": "^1.23",
"symfony/deprecation-contracts": "^2.2|^3.0",
"symfony/event-dispatcher": "^6.4|^7.0",
"symfony/framework-bundle": "^6.4|^7.0",
"symfony/property-access": "^6.4|^7.0",
"symfony/property-info": "^6.4|^7.0",
Expand Down
1 change: 1 addition & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
service('.zenstruck_foundry.instantiator'),
service('.zenstruck_foundry.story_registry'),
service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
service('event_dispatcher'),
])
->public()
;
Expand Down
70 changes: 58 additions & 12 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -552,10 +552,10 @@ random data for your factories:
faker:
service: my_faker # service id for your own instance of Faker\Generator
Events / Hooks
~~~~~~~~~~~~~~
Hooks
~~~~~

The following events can be added to factories. Multiple event callbacks can be added, they are run in the order
The following hooks can be added to factories. Multiple hooks callbacks can be added, they are run in the order
they were added.

::
Expand All @@ -564,28 +564,28 @@ they were added.
use Zenstruck\Foundry\Proxy;

PostFactory::new()
->beforeInstantiate(function(array $attributes, string $class, static $factory): array {
// $attributes is what will be used to instantiate the object, manipulate as required
->beforeInstantiate(function(array $parameters, string $class, static $factory): array {
// $parameters is what will be used to instantiate the object, manipulate as required
// $class is the class of the object being instantiated
// $factory is the factory instance which creates the object
$attributes['title'] = 'Different title';
$parameters['title'] = 'Different title';

return $attributes; // must return the final $attributes
return $parameters; // must return the final $parameters
})
->afterInstantiate(function(Post $object, array $attributes, static $factory): void {
->afterInstantiate(function(Post $object, array $parameters, static $factory): void {
// $object is the instantiated object
// $attributes contains the attributes used to instantiate the object and any extras
// $parameters contains the attributes used to instantiate the object and any extras
// $factory is the factory instance which creates the object
})
->afterPersist(function(Post $object, array $attributes, static $factory) {
->afterPersist(function(Post $object, array $parameters, static $factory) {
// this event is only called if the object was persisted
// $object is the persisted Post object
// $attributes contains the attributes used to instantiate the object and any extras
// $parameters contains the attributes used to instantiate the object and any extras
// $factory is the factory instance which creates the object
})

// multiple events are allowed
->beforeInstantiate(function($attributes) { return $attributes; })
->beforeInstantiate(function($parameters) { return $parameters; })
->afterInstantiate(function() {})
->afterPersist(function() {})
;
Expand All @@ -603,6 +603,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
~~~~~~~~~~~~~~

Expand Down
12 changes: 12 additions & 0 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,6 +52,7 @@ public function __construct(
callable $instantiator,
public readonly StoryRegistry $stories,
private readonly ?PersistenceManager $persistence = null,
private readonly ?EventDispatcherInterface $eventDispatcher = null,
) {
$this->instantiator = $instantiator;
}
Expand Down Expand Up @@ -80,6 +82,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;
Expand Down
34 changes: 34 additions & 0 deletions src/Object/Event/AfterInstantiate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

/*
* This file is part of the zenstruck/foundry package.
*
* (c) Kevin Bond <[email protected]>
*
* 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 <[email protected]>
*
* @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<object> */
public readonly ObjectFactory $factory,
) {
}
}
35 changes: 35 additions & 0 deletions src/Object/Event/BeforeInstantiate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

/*
* This file is part of the zenstruck/foundry package.
*
* (c) Kevin Bond <[email protected]>
*
* 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 <[email protected]>
*
* @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<object> */
public readonly ObjectFactory $factory,
) {
}
}
29 changes: 29 additions & 0 deletions src/ObjectFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Zenstruck\Foundry;

use Zenstruck\Foundry\Object\Event\AfterInstantiate;
use Zenstruck\Foundry\Object\Event\BeforeInstantiate;
use Zenstruck\Foundry\Object\Instantiator;

/**
Expand Down Expand Up @@ -102,4 +104,31 @@ public function afterInstantiate(callable $callback): static

return $clone;
}

/**
* @internal
*/
protected function initializeInternal(): static
{
if (!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)
);
}
);
}
}
34 changes: 34 additions & 0 deletions src/Persistence/Event/AfterPersist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

/*
* This file is part of the zenstruck/foundry package.
*
* (c) Kevin Bond <[email protected]>
*
* 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 <[email protected]>
*
* @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<object> */
public readonly PersistentObjectFactory $factory,
) {
}
}
32 changes: 22 additions & 10 deletions src/Persistence/PersistentObjectFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Zenstruck\Foundry\Factory;
use Zenstruck\Foundry\FactoryCollection;
use Zenstruck\Foundry\ObjectFactory;
use Zenstruck\Foundry\Persistence\Event\AfterPersist;
use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects;
use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed;

Expand Down Expand Up @@ -406,19 +407,30 @@ final protected function isPersisting(): bool
return $this->persistMode()->isPersisting();
}

/**
* Schedule any new object for insert right after instantiation.
* @internal
*/
final protected function initializeInternal(): static
{
return $this->afterInstantiate(
static function(object $object, array $parameters, PersistentObjectFactory $factory): void {
if (!$factory->isPersisting()) {
return;
// Schedule any new object for insert right after instantiation
$factory = parent::initializeInternal()
->afterInstantiate(
static function(object $object, array $parameters, PersistentObjectFactory $factoryUsed): void {
if (!$factoryUsed->isPersisting()) {
return;
}

Configuration::instance()->persistence()->scheduleForInsert($object);
}

Configuration::instance()->persistence()->scheduleForInsert($object);
);

if (!Configuration::instance()->hasEventDispatcher()) {
return $factory;
};

// Dispatch event after persist
return $factory->afterPersist(
static function(object $object, array $parameters, self $factoryUsed): void {
Configuration::instance()->eventDispatcher()->dispatch(
new AfterPersist($object, $parameters, $factoryUsed)
);
}
);
}
Expand Down
27 changes: 27 additions & 0 deletions tests/Fixture/Entity/EntityForEventListeners.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

/*
* This file is part of the zenstruck/foundry package.
*
* (c) Kevin Bond <[email protected]>
*
* 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,
) {
}
}
Loading

0 comments on commit 34101a7

Please sign in to comment.