Skip to content

Commit 62e6357

Browse files
committed
feat: introduce attribute #[AsFoudryHook]
1 parent 4d0341b commit 62e6357

File tree

10 files changed

+263
-23
lines changed

10 files changed

+263
-23
lines changed

docs/index.rst

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -644,11 +644,11 @@ 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-
~~~~~~
647+
Hooks as service / global hooks
648+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
649649

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

653653
::
654654

@@ -657,38 +657,44 @@ allowing to create hooks globally, as Symfony services:
657657
use Zenstruck\Foundry\Object\Event\BeforeInstantiate;
658658
use Zenstruck\Foundry\Persistence\Event\AfterPersist;
659659

660-
final class FoundryEventListener
660+
final class FoundryHook
661661
{
662-
#[AsEventListener]
662+
#[AsFoundryHook(Post::class)]
663663
public function beforeInstantiate(BeforeInstantiate $event): void
664664
{
665-
// do something before the object is instantiated:
665+
// do something before the post is instantiated:
666666
// $event->parameters is what will be used to instantiate the object, manipulate as required
667667
// $event->objectClass is the class of the object being instantiated
668668
// $event->factory is the factory instance which creates the object
669669
}
670670

671-
#[AsEventListener]
671+
#[AsFoundryHook(Post::class)]
672672
public function afterInstantiate(AfterInstantiate $event): void
673673
{
674-
// $event->object is the instantiated object
674+
// $event->object is the instantiated Post object
675675
// $event->parameters contains the attributes used to instantiate the object and any extras
676676
// $event->factory is the factory instance which creates the object
677677
}
678678

679-
#[AsEventListener]
679+
#[AsFoundryHook(Post::class)]
680680
public function afterPersist(AfterPersist $event): void
681681
{
682682
// this event is only called if the object was persisted
683683
// $event->object is the persisted Post object
684684
// $event->parameters contains the attributes used to instantiate the object and any extras
685685
// $event->factory is the factory instance which creates the object
686686
}
687+
688+
#[AsFoundryHook]
689+
public function afterInstantiateGlobal(AfterInstantiate $event): void
690+
{
691+
// Omitting class defines a "global" hook which will be called for all objects
692+
}
687693
}
688694

689695
.. versionadded:: 2.4
690696

691-
Those events are triggered since Foundry 2.4.
697+
The ``#[AsFoundryHook]`` attribute was added in Foundry 2.4.
692698

693699
Initialization
694700
~~~~~~~~~~~~~~

src/Attribute/AsFoundryHook.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 <kevinbond@gmail.com>
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\Attribute;
15+
16+
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
17+
18+
#[\Attribute(\Attribute::TARGET_METHOD)]
19+
final class AsFoundryHook extends AsEventListener
20+
{
21+
public function __construct(
22+
/** @var class-string */
23+
public readonly ?string $objectClass = null,
24+
int $priority = 0,
25+
) {
26+
parent::__construct(priority: $priority);
27+
}
28+
}

src/Object/Event/AfterInstantiate.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,25 @@
1919
/**
2020
* @author Nicolas PHILIPPE <nikophil@gmail.com>
2121
*
22+
* @template T of object
23+
* @implements Event<T>
24+
*
2225
* @phpstan-import-type Parameters from Factory
2326
*/
24-
final class AfterInstantiate
27+
final class AfterInstantiate implements Event
2528
{
2629
public function __construct(
30+
/** @var T */
2731
public readonly object $object,
2832
/** @phpstan-var Parameters */
2933
public readonly array $parameters,
30-
/** @var ObjectFactory<object> */
34+
/** @var ObjectFactory<T> */
3135
public readonly ObjectFactory $factory,
3236
) {
3337
}
38+
39+
public function objectClassName(): string
40+
{
41+
return $this->object::class;
42+
}
3443
}

src/Object/Event/BeforeInstantiate.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,25 @@
1919
/**
2020
* @author Nicolas PHILIPPE <nikophil@gmail.com>
2121
*
22+
* @template T of object
23+
* @implements Event<T>
24+
*
2225
* @phpstan-import-type Parameters from Factory
2326
*/
24-
final class BeforeInstantiate
27+
final class BeforeInstantiate implements Event
2528
{
2629
public function __construct(
2730
/** @phpstan-var Parameters */
2831
public array $parameters,
29-
/** @var class-string */
32+
/** @var class-string<T> */
3033
public readonly string $objectClass,
31-
/** @var ObjectFactory<object> */
34+
/** @var ObjectFactory<T> */
3235
public readonly ObjectFactory $factory,
3336
) {
3437
}
38+
39+
public function objectClassName(): string
40+
{
41+
return $this->objectClass;
42+
}
3543
}

src/Object/Event/Event.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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 <kevinbond@gmail.com>
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+
/**
17+
* @template T of object
18+
*/
19+
interface Event
20+
{
21+
/**
22+
* @return class-string<T>
23+
*/
24+
public function objectClassName(): string;
25+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 <kevinbond@gmail.com>
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+
final class HookListenerFilter
17+
{
18+
/** @var \Closure(Event<object>): void */
19+
private \Closure $listener;
20+
21+
/**
22+
* @param array{0: object, 1: string} $listener
23+
* @param class-string|null $objectClass
24+
*/
25+
public function __construct(array $listener, private ?string $objectClass = null)
26+
{
27+
if (!\is_callable($listener)) {
28+
throw new \InvalidArgumentException(\sprintf('Listener must be a callable, "%s" given.', \get_debug_type($listener)));
29+
}
30+
31+
$this->listener = $listener(...);
32+
}
33+
34+
/**
35+
* @param Event<object> $event
36+
*/
37+
public function __invoke(Event $event): void
38+
{
39+
if ($this->objectClass && $event->objectClassName() !== $this->objectClass) {
40+
return;
41+
}
42+
43+
($this->listener)($event);
44+
}
45+
}

src/Persistence/Event/AfterPersist.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,31 @@
1414
namespace Zenstruck\Foundry\Persistence\Event;
1515

1616
use Zenstruck\Foundry\Factory;
17+
use Zenstruck\Foundry\Object\Event\Event;
1718
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
1819

1920
/**
2021
* @author Nicolas PHILIPPE <nikophil@gmail.com>
2122
*
23+
* @template T of object
24+
* @implements Event<T>
25+
*
2226
* @phpstan-import-type Parameters from Factory
2327
*/
24-
final class AfterPersist
28+
final class AfterPersist implements Event
2529
{
2630
public function __construct(
31+
/** @var T */
2732
public readonly object $object,
2833
/** @phpstan-var Parameters */
2934
public readonly array $parameters,
30-
/** @var PersistentObjectFactory<object> */
35+
/** @var PersistentObjectFactory<T> */
3136
public readonly PersistentObjectFactory $factory,
3237
) {
3338
}
39+
40+
public function objectClassName(): string
41+
{
42+
return $this->object::class;
43+
}
3444
}

src/ZenstruckFoundryBundle.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,19 @@
1515
use Symfony\Component\DependencyInjection\ChildDefinition;
1616
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1717
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
use Symfony\Component\DependencyInjection\Definition;
1819
use Symfony\Component\DependencyInjection\Exception\LogicException;
1920
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
2021
use Symfony\Component\DependencyInjection\Reference;
2122
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
23+
use Zenstruck\Foundry\Attribute\AsFoundryHook;
2224
use Zenstruck\Foundry\Attribute\AsFixture;
2325
use Zenstruck\Foundry\DependencyInjection\AsFixtureStoryCompilerPass;
2426
use Zenstruck\Foundry\InMemory\DependencyInjection\InMemoryCompilerPass;
2527
use Zenstruck\Foundry\InMemory\InMemoryRepository;
2628
use Zenstruck\Foundry\Mongo\MongoResetter;
29+
use Zenstruck\Foundry\Object\Event\Event;
30+
use Zenstruck\Foundry\Object\Event\HookListenerFilter;
2731
use Zenstruck\Foundry\Object\Instantiator;
2832
use Zenstruck\Foundry\ORM\ResetDatabase\MigrateDatabaseResetter;
2933
use Zenstruck\Foundry\ORM\ResetDatabase\OrmResetter;
@@ -238,6 +242,25 @@ public function loadExtension(array $config, ContainerConfigurator $configurator
238242
$this->configureInMemory($configurator, $container);
239243
$this->configureFixturesStory($container);
240244
$this->configureAutoRefreshWithLazyObjects($container, $config['enable_auto_refresh_with_lazy_objects'] ?? null);
245+
246+
$container->registerAttributeForAutoconfiguration(
247+
AsFoundryHook::class,
248+
// @phpstan-ignore argument.type
249+
static function(ChildDefinition $definition, AsFoundryHook $attribute, \ReflectionMethod $reflector) {
250+
if (1 !== \count($reflector->getParameters())
251+
|| !$reflector->getParameters()[0]->getType()
252+
|| !$reflector->getParameters()[0]->getType() instanceof \ReflectionNamedType
253+
|| !\is_a($reflector->getParameters()[0]->getType()->getName(), Event::class, true)
254+
) {
255+
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));
256+
}
257+
$definition->addTag('foundry.hook', [
258+
'class' => $attribute->objectClass,
259+
'method' => $reflector->getName(),
260+
'event' => $reflector->getParameters()[0]->getType()->getName(),
261+
]);
262+
}
263+
);
241264
}
242265

243266
public function build(ContainerBuilder $container): void
@@ -258,6 +281,21 @@ public function process(ContainerBuilder $container): void
258281
->addMethodCall('addProvider', [new Reference($id)])
259282
;
260283
}
284+
285+
// events
286+
$i = 0;
287+
foreach ($container->findTaggedServiceIds('foundry.hook') as $id => $tags) {
288+
foreach ($tags as $tag) {
289+
$container
290+
->setDefinition("foundry.hook.{$tag['event']}.{$i}", new Definition(class: HookListenerFilter::class))
291+
->setArgument(0, [new Reference($id), $tag['method']])
292+
->setArgument(1, $tag['class'])
293+
->addTag('kernel.event_listener', ['event' => $tag['event']])
294+
;
295+
296+
++$i;
297+
}
298+
}
261299
}
262300

263301
/**

0 commit comments

Comments
 (0)