Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce Foundry events #790

Merged
merged 1 commit into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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