Skip to content

Commit

Permalink
feature: PHP Attributes for Bootloader Methods
Browse files Browse the repository at this point in the history
  • Loading branch information
butschster committed Jan 5, 2025
1 parent 00eb378 commit 0c393f9
Show file tree
Hide file tree
Showing 48 changed files with 1,450 additions and 183 deletions.
5 changes: 5 additions & 0 deletions src/Boot/src/AbstractKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use Spiral\Boot\Bootloader\BootloaderRegistry;
use Spiral\Boot\Bootloader\BootloaderRegistryInterface;
use Spiral\Boot\Bootloader\CoreBootloader;
use Spiral\Boot\BootloadManager\AttributeResolver;
use Spiral\Boot\BootloadManager\AttributeResolverRegistryInterface;
use Spiral\Boot\BootloadManager\StrategyBasedBootloadManager;
use Spiral\Boot\BootloadManager\DefaultInvokerStrategy;
use Spiral\Boot\BootloadManager\Initializer;
Expand Down Expand Up @@ -128,9 +130,12 @@ final public static function create(
$exceptionHandler->register();
}

$container->bind(AttributeResolverRegistryInterface::class, AttributeResolver::class);

if (!$container->has(InitializerInterface::class)) {
$container->bind(InitializerInterface::class, Initializer::class);
}

if (!$container->has(InvokerStrategyInterface::class)) {
$container->bind(InvokerStrategyInterface::class, DefaultInvokerStrategy::class);
}
Expand Down
23 changes: 23 additions & 0 deletions src/Boot/src/Attribute/AbstractMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\Attribute;

abstract class AbstractMethod
{
/**
* @param non-empty-string|null $alias
*/
public function __construct(
/**
* Alias for the method.
* If not provided, the return type of the method will be used as the alias.
*/
public readonly ?string $alias = null,
/**
* Add aliases from the return type of the method even if the method has an alias.
*/
public readonly bool $aliasesFromReturnType = false,
) {}
}
19 changes: 19 additions & 0 deletions src/Boot/src/Attribute/BindAlias.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\Attribute;

#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class BindAlias
{
/**
* @param non-empty-string[] $aliases
*/
public readonly array $aliases;

public function __construct(string ...$aliases)
{
$this->aliases = $aliases;
}
}
8 changes: 8 additions & 0 deletions src/Boot/src/Attribute/BindMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\Attribute;

#[\Attribute(\Attribute::TARGET_METHOD)]
final class BindMethod extends AbstractMethod {}
16 changes: 16 additions & 0 deletions src/Boot/src/Attribute/BindScope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\Attribute;

#[\Attribute(\Attribute::TARGET_METHOD)]
final class BindScope
{
public readonly string $scope;

public function __construct(string|\BackedEnum $scope)
{
$this->scope = \is_object($scope) ? (string) $scope->value : $scope;
}
}
13 changes: 13 additions & 0 deletions src/Boot/src/Attribute/BootMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\Attribute;

#[\Attribute(\Attribute::TARGET_METHOD)]
final class BootMethod
{
public function __construct(
public readonly int $priority = 0,
) {}
}
7 changes: 2 additions & 5 deletions src/Boot/src/Attribute/BootloadConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

namespace Spiral\Boot\Attribute;

use Spiral\Attributes\NamedArgumentConstructor;

#[\Attribute(\Attribute::TARGET_CLASS), NamedArgumentConstructor]
#[\Attribute(\Attribute::TARGET_CLASS)]
class BootloadConfig
{
public function __construct(
Expand All @@ -15,6 +13,5 @@ public function __construct(
public array $allowEnv = [],
public array $denyEnv = [],
public bool $override = true,
) {
}
) {}
}
13 changes: 13 additions & 0 deletions src/Boot/src/Attribute/InitMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\Attribute;

#[\Attribute(\Attribute::TARGET_METHOD)]
final class InitMethod
{
public function __construct(
public readonly int $priority = 0,
) {}
}
14 changes: 14 additions & 0 deletions src/Boot/src/Attribute/InjectorMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\Attribute;

#[\Attribute(\Attribute::TARGET_METHOD)]
final class InjectorMethod extends AbstractMethod
{
public function __construct(string $alias)
{
parent::__construct($alias);
}
}
8 changes: 8 additions & 0 deletions src/Boot/src/Attribute/SingletonMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\Attribute;

#[\Attribute(\Attribute::TARGET_METHOD)]
final class SingletonMethod extends AbstractMethod {}
62 changes: 62 additions & 0 deletions src/Boot/src/BootloadManager/AttributeResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\BootloadManager;

use Spiral\Boot\Attribute\AbstractMethod;
use Spiral\Boot\Attribute\BindMethod;
use Spiral\Boot\Attribute\InjectorMethod;
use Spiral\Boot\Attribute\SingletonMethod;
use Spiral\Boot\Bootloader\BootloaderInterface;
use Spiral\Core\Attribute\Singleton;
use Spiral\Core\Container;

/**
* @internal
* @template TAttribute of AbstractMethod
* @template TBootloader of BootloaderInterface
* @implements AttributeResolverInterface<TAttribute, TBootloader>
* @implements AttributeResolverRegistryInterface<TAttribute>
*/
#[Singleton]
final class AttributeResolver implements AttributeResolverInterface, AttributeResolverRegistryInterface
{
/**
* @var array<class-string<TAttribute>, AttributeResolverInterface>
*/
private array $resolvers = [];

public function __construct(Container $container)
{
/** @psalm-suppress InvalidArgument */
$this->register(SingletonMethod::class, $container->get(AttributeResolver\SingletonMethodResolver::class));
/** @psalm-suppress InvalidArgument */
$this->register(BindMethod::class, $container->get(AttributeResolver\BindMethodResolver::class));
/** @psalm-suppress InvalidArgument */
$this->register(InjectorMethod::class, $container->get(AttributeResolver\InjectorMethodResolver::class));
}

public function register(string $attribute, AttributeResolverInterface $resolver): void
{
$this->resolvers[$attribute] = $resolver;
}

/**
* @return class-string<TAttribute>[]
*/
public function getResolvers(): array
{
return \array_keys($this->resolvers);
}

public function resolve(object $attribute, object $service, \ReflectionMethod $method): void
{
$attributeClass = $attribute::class;
if (!isset($this->resolvers[$attributeClass])) {
throw new \RuntimeException("No resolver for attribute {$attributeClass}");
}

$this->resolvers[$attributeClass]->resolve($attribute, $service, $method);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\BootloadManager\AttributeResolver;

use Spiral\Boot\Attribute\AbstractMethod;
use Spiral\Boot\Attribute\BindAlias;
use Spiral\Boot\Attribute\BindScope;
use Spiral\Boot\Bootloader\BootloaderInterface;
use Spiral\Boot\BootloadManager\AttributeResolverInterface;
use Spiral\Core\BinderInterface;
use Spiral\Core\Config\Binding;

/**
* @template T of AbstractMethod
* @template TBootloader of BootloaderInterface
* @implements AttributeResolverInterface<T, TBootloader>
*/
abstract class AbstractResolver implements AttributeResolverInterface
{
public function __construct(
protected readonly BinderInterface $binder,
) {}

/**
* @psalm-param T $attribute
* @return list<non-empty-string>
*/
protected function getAliases(object $attribute, \ReflectionMethod $method): array
{
$alias = $attribute->alias ?? null;

$aliases = [];
if ($alias !== null) {
$aliases[] = $alias;
}

$attrs = $method->getAttributes(name: BindAlias::class);
foreach ($attrs as $attr) {
$aliases = [...$aliases, ...$attr->newInstance()->aliases];
}

// If no aliases are provided, we will use the return type as the alias.
if (\count($aliases) > 0 && !$attribute->aliasesFromReturnType) {
return \array_unique(\array_filter($aliases));
}

$type = $method->getReturnType();

if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
foreach ($type->getTypes() as $type) {
if ($type->isBuiltin()) {
continue;
}

$aliases[] = $type->getName();
}
} elseif ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
$aliases[] = $type->getName();
}

if ($aliases === []) {
throw new \LogicException(
"No alias provided for binding {$method->getDeclaringClass()->getName()}::{$method->getName()}",
);
}

return \array_unique(\array_filter($aliases));
}

protected function getScope(\ReflectionMethod $method): ?string
{
$attrs = $method->getAttributes(name: BindScope::class);

if ($attrs === []) {
return null;
}

return $attrs[0]->newInstance()->scope;
}

protected function bind(array $aliases, Binding $binding, ?string $scope = null): void
{
$binder = $this->binder->getBinder($scope);

$alias = \array_shift($aliases);
foreach ($aliases as $a) {
$binder->bind($alias, $a);
$alias = \array_shift($aliases);
}

$binder->bind($alias, $binding);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\BootloadManager\AttributeResolver;

use Spiral\Boot\Attribute\BindMethod;
use Spiral\Boot\Bootloader\BootloaderInterface;
use Spiral\Core\Config\Factory;

/**
* @internal
* @extends AbstractResolver<BindMethod, BootloaderInterface>
*/
final class BindMethodResolver extends AbstractResolver
{
public function resolve(object $attribute, object $service, \ReflectionMethod $method): void
{
$aliases = $this->getAliases($attribute, $method);
$closure = new Factory(
callable: $method->getClosure($service),
singleton: false,
);

$this->bind($aliases, $closure, $this->getScope($method));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\BootloadManager\AttributeResolver;

use Spiral\Boot\Attribute\InjectorMethod;
use Spiral\Boot\Bootloader\BootloaderInterface;
use Spiral\Boot\BootloadManager\AttributeResolverInterface;
use Spiral\Core\BinderInterface;
use Spiral\Core\Config\Injectable;
use Spiral\Core\InvokerInterface;

/**
* @internal
* @implements AttributeResolverInterface<InjectorMethod, BootloaderInterface>
*/
final class InjectorMethodResolver implements AttributeResolverInterface
{
public function __construct(
private readonly BinderInterface $binder,
private readonly InvokerInterface $invoker,
) {}

public function resolve(object $attribute, object $service, \ReflectionMethod $method): void
{
$this->binder->bind(
$attribute->alias,
new Injectable($this->invoker->invoke($method->getClosure($service))),
);
}
}
Loading

0 comments on commit 0c393f9

Please sign in to comment.