Skip to content

Commit 2b0f3a1

Browse files
committed
feat: introduce hook services
1 parent 200cfdd commit 2b0f3a1

23 files changed

+738
-11
lines changed

config/services.php

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Faker;
66
use Zenstruck\Foundry\Configuration;
77
use Zenstruck\Foundry\FactoryRegistry;
8+
use Zenstruck\Foundry\Hooks\HooksRegistry;
89
use Zenstruck\Foundry\Object\Instantiator;
910
use Zenstruck\Foundry\StoryRegistry;
1011

@@ -32,7 +33,13 @@
3233
service('.zenstruck_foundry.instantiator'),
3334
service('.zenstruck_foundry.story_registry'),
3435
service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
36+
service('.zenstruck_foundry.hooks.registry')->nullOnInvalid(),
3537
])
3638
->public()
39+
40+
->set('.zenstruck_foundry.hooks.registry', HooksRegistry::class)
41+
->args([
42+
abstract_arg('hooks_service_locator'),
43+
])
3744
;
3845
};

src/Configuration.php

+12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Zenstruck\Foundry\Exception\FoundryNotBooted;
1616
use Zenstruck\Foundry\Exception\PersistenceDisabled;
1717
use Zenstruck\Foundry\Exception\PersistenceNotAvailable;
18+
use Zenstruck\Foundry\Hooks\HooksRegistry;
1819
use Zenstruck\Foundry\Persistence\PersistenceManager;
1920

2021
/**
@@ -50,6 +51,7 @@ public function __construct(
5051
callable $instantiator,
5152
public readonly StoryRegistry $stories,
5253
private readonly ?PersistenceManager $persistence = null,
54+
private readonly ?HooksRegistry $hooksRegistry = null,
5355
) {
5456
$this->instantiator = $instantiator;
5557
}
@@ -79,6 +81,16 @@ public function assertPersistenceEnabled(): void
7981
}
8082
}
8183

84+
public function isHooksRegistry(): bool
85+
{
86+
return (bool) $this->hooksRegistry;
87+
}
88+
89+
public function hooksRegistry(): HooksRegistry
90+
{
91+
return $this->hooksRegistry ?? throw new \LogicException('Cannot get hooks registry. Note: hooks cannot be used in unit tests.');
92+
}
93+
8294
public function inADataProvider(): bool
8395
{
8496
return $this->bootedForDataProvider;

src/Hooks/AfterInstantiate.php

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 <[email protected]>
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\Hooks;
15+
16+
use Zenstruck\Foundry\Factory;
17+
use Zenstruck\Foundry\ObjectFactory;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <[email protected]>
21+
*
22+
* @phpstan-import-type Parameters from Factory
23+
* @template T of object
24+
* @implements HookEvent<T>
25+
*/
26+
final class AfterInstantiate implements HookEvent
27+
{
28+
public function __construct(
29+
/** @var T */
30+
public readonly object $object,
31+
/** @phpstan-var Parameters */
32+
public readonly array $parameters,
33+
/** @var ObjectFactory<T> */
34+
public readonly ObjectFactory $factory,
35+
) {
36+
}
37+
38+
public function getObjectClass(): string
39+
{
40+
return $this->object::class;
41+
}
42+
}

src/Hooks/AfterPersist.php

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 <[email protected]>
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\Hooks;
15+
16+
use Zenstruck\Foundry\Factory;
17+
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <[email protected]>
21+
*
22+
* @phpstan-import-type Parameters from Factory
23+
* @template T of object
24+
* @implements HookEvent<T>
25+
*/
26+
final class AfterPersist implements HookEvent
27+
{
28+
public function __construct(
29+
/** @var T */
30+
public readonly object $object,
31+
/** @phpstan-var Parameters */
32+
public readonly array $parameters,
33+
/** @var PersistentObjectFactory<T> */
34+
public readonly PersistentObjectFactory $factory,
35+
) {
36+
}
37+
38+
public function getObjectClass(): string
39+
{
40+
return $this->object::class;
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 <[email protected]>
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\Hooks;
15+
16+
/**
17+
* @author Nicolas PHILIPPE <[email protected]>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS)]
20+
final class AsAfterInstantiateFoundryHook
21+
{
22+
public function __construct(
23+
public ?string $class = null,
24+
) {
25+
}
26+
}
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 <[email protected]>
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\Hooks;
15+
16+
/**
17+
* @author Nicolas PHILIPPE <[email protected]>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS)]
20+
final class AsAfterPersistFoundryHook
21+
{
22+
public function __construct(
23+
public ?string $class = null,
24+
) {
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 <[email protected]>
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\Hooks;
15+
16+
/**
17+
* @author Nicolas PHILIPPE <[email protected]>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS)]
20+
final class AsBeforeInstantiateFoundryHook
21+
{
22+
public function __construct(
23+
public ?string $class = null,
24+
) {
25+
}
26+
}

src/Hooks/BeforeInstantiate.php

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 <[email protected]>
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\Hooks;
15+
16+
use Zenstruck\Foundry\Factory;
17+
use Zenstruck\Foundry\ObjectFactory;
18+
19+
/**
20+
* @author Nicolas PHILIPPE <[email protected]>
21+
*
22+
* @phpstan-import-type Parameters from Factory
23+
* @template T of object
24+
* @implements HookEvent<T>
25+
*/
26+
final class BeforeInstantiate implements HookEvent
27+
{
28+
public function __construct(
29+
/** @phpstan-var Parameters */
30+
public array $parameters,
31+
/** @var class-string<T> */
32+
public readonly string $objectClass,
33+
/** @var ObjectFactory<T> */
34+
public readonly ObjectFactory $factory,
35+
) {
36+
}
37+
38+
public function getObjectClass(): string
39+
{
40+
return $this->objectClass;
41+
}
42+
}

src/Hooks/HookEvent.php

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 <[email protected]>
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\Hooks;
15+
16+
/**
17+
* @author Nicolas PHILIPPE <[email protected]>
18+
*
19+
* @internal
20+
* @template T of object
21+
*/
22+
interface HookEvent
23+
{
24+
/** @return class-string<T> */
25+
public function getObjectClass(): string;
26+
}

src/Hooks/HooksRegistry.php

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 <[email protected]>
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\Hooks;
15+
16+
use Symfony\Component\DependencyInjection\ServiceLocator;
17+
18+
/**
19+
* @author Nicolas PHILIPPE <[email protected]>
20+
*
21+
* @internal
22+
*/
23+
final class HooksRegistry
24+
{
25+
public function __construct(
26+
/** @var ServiceLocator<list<callable(HookEvent<object>): void>> */
27+
private ServiceLocator $serviceLocator,
28+
) {
29+
}
30+
31+
/**
32+
* @param HookEvent<object> $hookEvent
33+
*/
34+
public function callHooks(HookEvent $hookEvent): void
35+
{
36+
foreach ($this->resolveHooks($hookEvent) as $hook) {
37+
($hook)($hookEvent);
38+
}
39+
}
40+
41+
public static function hookClassSpecificIndex(string $hookEventClass, string $objectClass): string
42+
{
43+
return "{$hookEventClass}-{$objectClass}";
44+
}
45+
46+
/**
47+
* @param HookEvent<object> $hookEvent
48+
* @return list<callable(HookEvent<object>): void>
49+
*/
50+
private function resolveHooks(HookEvent $hookEvent): array
51+
{
52+
$objectSpecificIndex = self::hookClassSpecificIndex($hookEvent::class, $hookEvent->getObjectClass());
53+
54+
return [
55+
...$this->serviceLocator->has($hookEvent::class) ? $this->serviceLocator->get($hookEvent::class) : [],
56+
...$this->serviceLocator->has($objectSpecificIndex) ? $this->serviceLocator->get($objectSpecificIndex) : [],
57+
];
58+
}
59+
}

src/ObjectFactory.php

+29
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Zenstruck\Foundry;
1313

14+
use Zenstruck\Foundry\Hooks\AfterInstantiate;
15+
use Zenstruck\Foundry\Hooks\BeforeInstantiate;
1416
use Zenstruck\Foundry\Object\Instantiator;
1517

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

103105
return $clone;
104106
}
107+
108+
/**
109+
* @internal
110+
*/
111+
protected function initializeInternal(): static
112+
{
113+
if (!Configuration::instance()->isHooksRegistry()) {
114+
return $this;
115+
}
116+
117+
return $this->beforeInstantiate(
118+
static function(array $parameters, string $objectClass, self $usedFactory): array {
119+
Configuration::instance()->hooksRegistry()->callHooks(
120+
$hook = new BeforeInstantiate($parameters, $objectClass, $usedFactory)
121+
);
122+
123+
return $hook->parameters;
124+
}
125+
)
126+
->afterInstantiate(
127+
static function(object $object, array $parameters, self $usedFactory): void {
128+
Configuration::instance()->hooksRegistry()->callHooks(
129+
new AfterInstantiate($object, $parameters, $usedFactory)
130+
);
131+
}
132+
);
133+
}
105134
}

0 commit comments

Comments
 (0)