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 hook services #790

Open
wants to merge 1 commit into
base: 2.x
Choose a base branch
from
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
7 changes: 7 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Faker;
use Zenstruck\Foundry\Configuration;
use Zenstruck\Foundry\FactoryRegistry;
use Zenstruck\Foundry\Hooks\HooksRegistry;
use Zenstruck\Foundry\Object\Instantiator;
use Zenstruck\Foundry\StoryRegistry;

Expand Down Expand Up @@ -32,7 +33,13 @@
service('.zenstruck_foundry.instantiator'),
service('.zenstruck_foundry.story_registry'),
service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
service('.zenstruck_foundry.hooks.registry')->nullOnInvalid(),
])
->public()

->set('.zenstruck_foundry.hooks.registry', HooksRegistry::class)
->args([
abstract_arg('hooks_service_locator'),
])
;
};
12 changes: 12 additions & 0 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Zenstruck\Foundry\Exception\FoundryNotBooted;
use Zenstruck\Foundry\Exception\PersistenceDisabled;
use Zenstruck\Foundry\Exception\PersistenceNotAvailable;
use Zenstruck\Foundry\Hooks\HooksRegistry;
use Zenstruck\Foundry\Persistence\PersistenceManager;

/**
Expand Down Expand Up @@ -50,6 +51,7 @@ public function __construct(
callable $instantiator,
public readonly StoryRegistry $stories,
private readonly ?PersistenceManager $persistence = null,
private readonly ?HooksRegistry $hooksRegistry = null,
) {
$this->instantiator = $instantiator;
}
Expand Down Expand Up @@ -79,6 +81,16 @@ public function assertPersistenceEnabled(): void
}
}

public function isHooksRegistry(): bool
{
return (bool) $this->hooksRegistry;
}

public function hooksRegistry(): HooksRegistry
{
return $this->hooksRegistry ?? throw new \LogicException('Cannot get hooks registry. Note: hooks cannot be used in unit tests.');
}

public function inADataProvider(): bool
{
return $this->bootedForDataProvider;
Expand Down
42 changes: 42 additions & 0 deletions src/Hooks/AfterInstantiate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?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\Hooks;

use Zenstruck\Foundry\Factory;
use Zenstruck\Foundry\ObjectFactory;

/**
* @author Nicolas PHILIPPE <[email protected]>
*
* @phpstan-import-type Parameters from Factory
* @template T of object
* @implements HookEvent<T>
*/
final class AfterInstantiate implements HookEvent
{
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 getObjectClass(): string
{
return $this->object::class;
}
}
42 changes: 42 additions & 0 deletions src/Hooks/AfterPersist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?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\Hooks;

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

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

public function getObjectClass(): string
{
return $this->object::class;
}
}
26 changes: 26 additions & 0 deletions src/Hooks/AsAfterInstantiateFoundryHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\Hooks;

/**
* @author Nicolas PHILIPPE <[email protected]>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
Copy link
Member Author

@nikophil nikophil Jan 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in a first time, I think this is enough.
Maybe in a second time, if someone needs it, we could implement TARGET_METHOD and IS_REPEATABLE, but this will complexify a lot the DI

final class AsAfterInstantiateFoundryHook
{
public function __construct(
public ?string $class = null,
) {
}
}
26 changes: 26 additions & 0 deletions src/Hooks/AsAfterPersistFoundryHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\Hooks;

/**
* @author Nicolas PHILIPPE <[email protected]>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class AsAfterPersistFoundryHook
{
public function __construct(
public ?string $class = null,
) {
}
}
26 changes: 26 additions & 0 deletions src/Hooks/AsBeforeInstantiateFoundryHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\Hooks;

/**
* @author Nicolas PHILIPPE <[email protected]>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class AsBeforeInstantiateFoundryHook
{
public function __construct(
public ?string $class = null,
) {
}
}
42 changes: 42 additions & 0 deletions src/Hooks/BeforeInstantiate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?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\Hooks;

use Zenstruck\Foundry\Factory;
use Zenstruck\Foundry\ObjectFactory;

/**
* @author Nicolas PHILIPPE <[email protected]>
*
* @phpstan-import-type Parameters from Factory
* @template T of object
* @implements HookEvent<T>
*/
final class BeforeInstantiate implements HookEvent
{
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 getObjectClass(): string
{
return $this->objectClass;
}
}
26 changes: 26 additions & 0 deletions src/Hooks/HookEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\Hooks;

/**
* @author Nicolas PHILIPPE <[email protected]>
*
* @internal
* @template T of object
*/
interface HookEvent
{
/** @return class-string<T> */
public function getObjectClass(): string;
}
59 changes: 59 additions & 0 deletions src/Hooks/HooksRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?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\Hooks;

use Symfony\Component\DependencyInjection\ServiceLocator;

/**
* @author Nicolas PHILIPPE <[email protected]>
*
* @internal
*/
final class HooksRegistry
{
public function __construct(
/** @var ServiceLocator<list<callable(HookEvent<object>): void>> */
private ServiceLocator $serviceLocator,
) {
}

/**
* @param HookEvent<object> $hookEvent
*/
public function callHooks(HookEvent $hookEvent): void
{
foreach ($this->resolveHooks($hookEvent) as $hook) {
($hook)($hookEvent);
}
}

public static function hookClassSpecificIndex(string $hookEventClass, string $objectClass): string
{
return "{$hookEventClass}-{$objectClass}";
}

/**
* @param HookEvent<object> $hookEvent
* @return list<callable(HookEvent<object>): void>
*/
private function resolveHooks(HookEvent $hookEvent): array
{
$objectSpecificIndex = self::hookClassSpecificIndex($hookEvent::class, $hookEvent->getObjectClass());

return [
...$this->serviceLocator->has($hookEvent::class) ? $this->serviceLocator->get($hookEvent::class) : [],
...$this->serviceLocator->has($objectSpecificIndex) ? $this->serviceLocator->get($objectSpecificIndex) : [],
];
}
}
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\Hooks\AfterInstantiate;
use Zenstruck\Foundry\Hooks\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()->isHooksRegistry()) {
return $this;
}

return $this->beforeInstantiate(
static function(array $parameters, string $objectClass, self $usedFactory): array {
Configuration::instance()->hooksRegistry()->callHooks(
$hook = new BeforeInstantiate($parameters, $objectClass, $usedFactory)
);

return $hook->parameters;
}
)
->afterInstantiate(
static function(object $object, array $parameters, self $usedFactory): void {
Configuration::instance()->hooksRegistry()->callHooks(
new AfterInstantiate($object, $parameters, $usedFactory)
);
}
);
}
}
Loading
Loading