Skip to content
Open
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
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -84,6 +85,7 @@
},
"conflict": {
"doctrine/persistence": "<2.0",
"symfony/event-dispatcher": "<6.4",
"symfony/framework-bundle": "<6.4"
},
"extra": {
Expand Down
4 changes: 3 additions & 1 deletion config/persistence.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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])
;
}
};
1 change: 1 addition & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
;
Expand Down
52 changes: 52 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,58 @@ You can also add hooks directly in your factory class:

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

Hooks as service / global hooks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For a better control of your hooks, you can define them as services, allowing to leverage dependency injection and
to create hooks globally:

::

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 FoundryHook
{
#[AsFoundryHook(Post::class)]
public function beforeInstantiate(BeforeInstantiate $event): void
{
// 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
}

#[AsFoundryHook(Post::class)]
public function afterInstantiate(AfterInstantiate $event): void
{
// $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
}

#[AsFoundryHook(Post::class)]
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
}

#[AsFoundryHook]
public function afterInstantiateGlobal(AfterInstantiate $event): void
{
// Omitting class defines a "global" hook which will be called for all objects
}
}

.. versionadded:: 2.4

The ``#[AsFoundryHook]`` attribute was added in Foundry 2.4.

Initialization
~~~~~~~~~~~~~~

Expand Down
28 changes: 28 additions & 0 deletions src/Attribute/AsFoundryHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?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\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);
}
}
14 changes: 13 additions & 1 deletion src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@
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;
use Zenstruck\Foundry\Exception\PersistenceNotAvailable;
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 <[email protected]>
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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;
Expand Down
43 changes: 43 additions & 0 deletions src/Object/Event/AfterInstantiate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?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]>
*
* @template T of object
* @implements Event<T>
*
* @phpstan-import-type Parameters from Factory
*/
final class AfterInstantiate implements Event
{
public function __construct(
/** @var T */
public readonly object $object,
/** @phpstan-var Parameters */
public readonly array $parameters,
/** @var ObjectFactory<T> */
public readonly ObjectFactory $factory,
) {
}

public function objectClassName(): string
{
return $this->object::class;
}
}
43 changes: 43 additions & 0 deletions src/Object/Event/BeforeInstantiate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?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]>
*
* @template T of object
* @implements Event<T>
*
* @phpstan-import-type Parameters from Factory
*/
final class BeforeInstantiate implements Event
{
public function __construct(
/** @phpstan-var Parameters */
public array $parameters,
/** @var class-string<T> */
public readonly string $objectClass,
/** @var ObjectFactory<T> */
public readonly ObjectFactory $factory,
) {
}

public function objectClassName(): string
{
return $this->objectClass;
}
}
25 changes: 25 additions & 0 deletions src/Object/Event/Event.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?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;

/**
* @template T of object
*/
interface Event
{
/**
* @return class-string<T>
*/
public function objectClassName(): string;
}
45 changes: 45 additions & 0 deletions src/Object/Event/HookListenerFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?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;

final class HookListenerFilter
{
/** @var \Closure(Event<object>): 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<object> $event
*/
public function __invoke(Event $event): void
{
if ($this->objectClass && $event->objectClassName() !== $this->objectClass) {
return;
}

($this->listener)($event);
}
}
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;
use Zenstruck\Foundry\Persistence\ProxyGenerator;

Expand Down Expand Up @@ -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<object>
* @internal
Expand Down
Loading