Skip to content

Commit

Permalink
feat: introduce hook services
Browse files Browse the repository at this point in the history
  • Loading branch information
nikophil committed Jan 18, 2025
1 parent 57c42bc commit 2934a44
Show file tree
Hide file tree
Showing 23 changed files with 738 additions and 11 deletions.
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 <kevinbond@gmail.com>
*
* 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 <nikophil@gmail.com>
*
* @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 <kevinbond@gmail.com>
*
* 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 <nikophil@gmail.com>
*
* @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 <kevinbond@gmail.com>
*
* 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 <nikophil@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
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 <kevinbond@gmail.com>
*
* 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 <nikophil@gmail.com>
*/
#[\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 <kevinbond@gmail.com>
*
* 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 <nikophil@gmail.com>
*/
#[\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 <kevinbond@gmail.com>
*
* 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 <nikophil@gmail.com>
*
* @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 <kevinbond@gmail.com>
*
* 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 <nikophil@gmail.com>
*
* @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 <kevinbond@gmail.com>
*
* 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 <nikophil@gmail.com>
*
* @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

0 comments on commit 2934a44

Please sign in to comment.