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 attribute #[AsFoundryHook] #802

Merged
merged 1 commit into from
Feb 4, 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
28 changes: 17 additions & 11 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -603,11 +603,11 @@ You can also add hooks directly in your factory class:

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

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

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:
For a better control of your hooks, you can define them as services, allowing to leverage dependency injection and
to create hooks globally:

::

Expand All @@ -616,38 +616,44 @@ allowing to create hooks globally, as Symfony services:
use Zenstruck\Foundry\Object\Event\BeforeInstantiate;
use Zenstruck\Foundry\Persistence\Event\AfterPersist;

final class FoundryEventListener
final class FoundryHook
{
#[AsEventListener]
#[AsFoundryHook(Post::class)]
public function beforeInstantiate(BeforeInstantiate $event): void
{
// do something before the object is instantiated:
// 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
}

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

#[AsEventListener]
#[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

Those events are triggered since Foundry 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);
}
}
13 changes: 11 additions & 2 deletions src/Object/Event/AfterInstantiate.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,25 @@
/**
* @author Nicolas PHILIPPE <[email protected]>
*
* @template T of object
* @implements Event<T>
*
* @phpstan-import-type Parameters from Factory
*/
final class AfterInstantiate
final class AfterInstantiate implements Event
{
public function __construct(
/** @var T */
public readonly object $object,
/** @phpstan-var Parameters */
public readonly array $parameters,
/** @var ObjectFactory<object> */
/** @var ObjectFactory<T> */
public readonly ObjectFactory $factory,
) {
}

public function objectClassName(): string
{
return $this->object::class;
}
}
14 changes: 11 additions & 3 deletions src/Object/Event/BeforeInstantiate.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,25 @@
/**
* @author Nicolas PHILIPPE <[email protected]>
*
* @template T of object
* @implements Event<T>
*
* @phpstan-import-type Parameters from Factory
*/
final class BeforeInstantiate
final class BeforeInstantiate implements Event
{
public function __construct(
/** @phpstan-var Parameters */
public array $parameters,
/** @var class-string */
/** @var class-string<T> */
public readonly string $objectClass,
/** @var ObjectFactory<object> */
/** @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)
nikophil marked this conversation as resolved.
Show resolved Hide resolved
{
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);
}
}
14 changes: 12 additions & 2 deletions src/Persistence/Event/AfterPersist.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,31 @@
namespace Zenstruck\Foundry\Persistence\Event;

use Zenstruck\Foundry\Factory;
use Zenstruck\Foundry\Object\Event\Event;
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;

/**
* @author Nicolas PHILIPPE <[email protected]>
*
* @template T of object
* @implements Event<T>
*
* @phpstan-import-type Parameters from Factory
*/
final class AfterPersist
final class AfterPersist implements Event
{
public function __construct(
/** @var T */
public readonly object $object,
/** @phpstan-var Parameters */
public readonly array $parameters,
/** @var PersistentObjectFactory<object> */
/** @var PersistentObjectFactory<T> */
public readonly PersistentObjectFactory $factory,
) {
}

public function objectClassName(): string
{
return $this->object::class;
}
}
40 changes: 40 additions & 0 deletions src/ZenstruckFoundryBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@
namespace Zenstruck\Foundry;

use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
use Zenstruck\Foundry\Attribute\AsFoundryHook;
use Zenstruck\Foundry\Mongo\MongoResetter;
use Zenstruck\Foundry\Object\Event\Event;
use Zenstruck\Foundry\Object\Event\HookListenerFilter;
use Zenstruck\Foundry\Object\Instantiator;
use Zenstruck\Foundry\ORM\ResetDatabase\MigrateDatabaseResetter;
use Zenstruck\Foundry\ORM\ResetDatabase\OrmResetter;
Expand Down Expand Up @@ -282,6 +288,25 @@ public function loadExtension(array $config, ContainerConfigurator $configurator
->replaceArgument(0, $config['mongo']['reset']['document_managers'])
;
}

$container->registerAttributeForAutoconfiguration(
AsFoundryHook::class,
// @phpstan-ignore argument.type
static function(ChildDefinition $definition, AsFoundryHook $attribute, \ReflectionMethod $reflector) {
if (1 !== \count($reflector->getParameters())
|| !$reflector->getParameters()[0]->getType()
|| !$reflector->getParameters()[0]->getType() instanceof \ReflectionNamedType
|| !\is_a($reflector->getParameters()[0]->getType()->getName(), Event::class, true)
) {
throw new LogicException(\sprintf("In order to use \"%s\" attribute, method \"{$reflector->class}::{$reflector->name}()\" must have a single parameter that is a subclass of \"%s\".", AsFoundryHook::class, Event::class));
}
$definition->addTag('foundry.hook', [
'class' => $attribute->objectClass,
'method' => $reflector->getName(),
'event' => $reflector->getParameters()[0]->getType()->getName(),
]);
}
);
}

public function build(ContainerBuilder $container): void
Expand All @@ -300,6 +325,21 @@ public function process(ContainerBuilder $container): void
->addMethodCall('addProvider', [new Reference($id)])
;
}

// events
$i = 0;
foreach ($container->findTaggedServiceIds('foundry.hook') as $id => $tags) {
foreach ($tags as $tag) {
$container
->setDefinition("foundry.hook.{$tag['event']}.{$i}", new Definition(class: HookListenerFilter::class))
->setArgument(0, [new Reference($id), $tag['method']])
->setArgument(1, $tag['class'])
->addTag('kernel.event_listener', ['event' => $tag['event']])
;

++$i;
}
}
}

/**
Expand Down
Loading