Skip to content

Commit

Permalink
Add tagged dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
brendt committed May 23, 2024
1 parent de09c31 commit a81e28d
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 25 deletions.
4 changes: 2 additions & 2 deletions src/Container/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface Container
{
public function register(string $className, callable $definition): self;

public function singleton(string $className, object|callable $definition): self;
public function singleton(string $className, object|callable $definition, ?string $tag = null): self;

public function config(object $config): self;

Expand All @@ -19,7 +19,7 @@ public function config(object $config): self;
* @param class-string<TClassName> $className
* @return TClassName
*/
public function get(string $className, mixed ...$params): object;
public function get(string $className, ?string $tag = null, mixed ...$params): object;

public function call(object $object, string $methodName, mixed ...$params): mixed;

Expand Down
6 changes: 6 additions & 0 deletions src/Container/Dependency.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ public function getTypeName(): string
{
$dependency = $this->dependency;

if (is_string($dependency)) {
$parts = explode('\\', $dependency);

return $parts[array_key_last($parts)];
}

return match($dependency::class) {
ReflectionClass::class => $dependency->getShortName(),
ReflectionMethod::class => $dependency->getDeclaringClass()->getShortName(),
Expand Down
57 changes: 57 additions & 0 deletions src/Container/Exceptions/CannotResolveTaggedDependency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Tempest\Container\Exceptions;

use Exception;
use Tempest\Container\Dependency;
use Tempest\Container\DependencyChain;

final class CannotResolveTaggedDependency extends Exception
{
public function __construct(DependencyChain $chain, Dependency $brokenDependency, string $tag)
{
$stack = $chain->all();
$stack[] = $brokenDependency;

$message = PHP_EOL . PHP_EOL. "Could not resolve tagged dependency {$brokenDependency->getName()}#{$tag}, did you forget to define an initializer for it?" . PHP_EOL;

if (count($stack) < 2) {
parent::__construct($message);

return;
}

$i = 0;

foreach ($stack as $currentDependency) {
$pipe = match ($i) {
0 => '┌──',
count($stack) - 1 => '└──',
default => '├──',
};

$message .= PHP_EOL . "\t{$pipe} " . $currentDependency->getShortName();

$i++;
}

$selectionLine = preg_replace_callback(
pattern: '/(?<prefix>(.*))(?<selection>'. $brokenDependency->getTypeName() .'\s\$\w+)(.*)/',
callback: function ($matches) {
return str_repeat(' ', strlen($matches['prefix']) + 4)
. str_repeat('', strlen($matches['selection']));
},
subject: $chain->last()->getShortName(),
);

$message .= PHP_EOL;
$message .= "\t{$selectionLine}";
$message .= PHP_EOL;
$message .= "Originally called in {$chain->getOrigin()}";
$message .= PHP_EOL;

parent::__construct($message);
}
}
77 changes: 56 additions & 21 deletions src/Container/GenericContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use ReflectionUnionType;
use Tempest\Container\Exceptions\CannotAutowireException;
use Tempest\Container\Exceptions\CannotInstantiateDependencyException;
use Tempest\Container\Exceptions\CannotResolveTaggedDependency;
use Tempest\Support\Reflection\Attributes;
use Throwable;

Expand Down Expand Up @@ -56,8 +57,10 @@ public function register(string $className, callable $definition): self
return $this;
}

public function singleton(string $className, object|callable $definition): self
public function singleton(string $className, object|callable $definition, ?string $tag = null): self
{
$className = $this->resolveTaggedName($className, $tag);

$this->singletons[$className] = $definition;

return $this;
Expand All @@ -70,11 +73,15 @@ public function config(object $config): self
return $this;
}

public function get(string $className, mixed ...$params): object
public function get(string $className, ?string $tag = null, mixed ...$params): object
{
$this->resolveChain();

$dependency = $this->resolve($className, ...$params);
$dependency = $this->resolve(
className: $className,
tag: $tag,
params: $params,
);

$this->stopChain();

Expand Down Expand Up @@ -110,9 +117,15 @@ public function addInitializer(ReflectionClass|string $initializerClass): Contai
return $this;
}

$initializeMethod = $initializerClass->getMethod('initialize');

// We resolve the optional Tag attribute from this initializer class
$tag = Attributes::find(Tag::class)->in($initializerClass)->first()
?? Attributes::find(Tag::class)->in($initializeMethod)->first();

// For normal Initializers, we'll use the return type
// to determine which dependency they resolve
$returnTypes = $initializerClass->getMethod('initialize')->getReturnType();
$returnTypes = $initializeMethod->getReturnType();

$returnTypes = match ($returnTypes::class) {
ReflectionNamedType::class => [$returnTypes],
Expand All @@ -121,16 +134,18 @@ public function addInitializer(ReflectionClass|string $initializerClass): Contai

/** @var ReflectionNamedType[] $returnTypes */
foreach ($returnTypes as $returnType) {
$this->initializers[$returnType->getName()] = $initializerClass->getName();
$this->initializers[$this->resolveTaggedName($returnType->getName(), $tag?->name)] = $initializerClass->getName();
}

return $this;
}

private function resolve(string $className, mixed ...$params): object
private function resolve(string $className, ?string $tag = null, mixed ...$params): object
{
$dependencyName = $this->resolveTaggedName($className, $tag);

// Check if the class has been registered as a singleton.
if ($instance = $this->singletons[$className] ?? null) {
if ($instance = $this->singletons[$dependencyName] ?? null) {
if (is_callable($instance)) {
$instance = $instance($this);
$this->singletons[$className] = $instance;
Expand All @@ -142,14 +157,14 @@ private function resolve(string $className, mixed ...$params): object
}

// Check if a callable has been registered to resolve this class.
if ($definition = $this->definitions[$className] ?? null) {
if ($definition = $this->definitions[$dependencyName] ?? null) {
$this->resolveChain()->add(new ReflectionFunction($definition));

return $definition($this);
}

// Next we check if any of our default initializers can initialize this class.
if ($initializer = $this->initializerFor($className)) {
if ($initializer = $this->initializerFor($className, $tag)) {
$this->resolveChain()->add(new ReflectionClass($initializer));

$object = match (true) {
Expand All @@ -158,29 +173,34 @@ private function resolve(string $className, mixed ...$params): object
};

// Check whether the initializer's result should be registered as a singleton
if (Attributes::find(Singleton::class)->in($initializer::class)->first() !== null) {
$this->singleton($className, $object);
$singleton = Attributes::find(Singleton::class)->in($initializer::class)->first()
?? Attributes::find(Singleton::class)->in((new ReflectionClass($initializer))->getMethod('initialize'))->first();

if ($singleton !== null) {
$this->singleton($className, $object, $tag);
}

return $object;
}

// If we're requesting a tagged dependency and haven't resolved it at this point, something's wrong
if ($tag) {
throw new CannotResolveTaggedDependency($this->chain, new Dependency($className), $tag);
}

// Finally, autowire the class.
return $this->autowire($className, ...$params);
}

private function initializerFor(string $className): null|Initializer|DynamicInitializer
private function initializerFor(string $className, ?string $tag = null): null|Initializer|DynamicInitializer
{
// Initializers themselves can't be initialized,
// otherwise you'd end up with infinite loops
if (
is_a($className, Initializer::class, true)
|| is_a($className, DynamicInitializer::class, true)
) {
if (is_a($className, Initializer::class, true) || is_a($className, DynamicInitializer::class, true)) {
return null;
}

if ($initializerClass = $this->initializers[$className] ?? null) {
if ($initializerClass = $this->initializers[$this->resolveTaggedName($className, $tag)] ?? null) {
return $this->resolve($initializerClass);
}

Expand Down Expand Up @@ -233,16 +253,19 @@ private function autowireDependencies(ReflectionMethod $method, array $parameter
// Build the class by iterating through its
// dependencies and resolving them.
foreach ($method->getParameters() as $parameter) {
$tag = Attributes::find(Tag::class)->in($parameter)->first();

$dependencies[] = $this->clone()->autowireDependency(
parameter: $parameter,
tag: $tag?->name,
providedValue: $parameters[$parameter->getName()] ?? null,
);
}

return $dependencies;
}

private function autowireDependency(ReflectionParameter $parameter, mixed $providedValue = null): mixed
private function autowireDependency(ReflectionParameter $parameter, ?string $tag, mixed $providedValue = null): mixed
{
$parameterType = $parameter->getType();

Expand All @@ -262,7 +285,12 @@ private function autowireDependency(ReflectionParameter $parameter, mixed $provi
// Loop through each type until we hit a match.
foreach ($types as $type) {
try {
return $this->autowireObjectDependency($type, $providedValue);
return $this->autowireObjectDependency(
/** @phpstan-ignore-next-line */
type: $type,
tag: $tag,
providedValue: $providedValue
);
} catch (Throwable $throwable) {
// We were unable to resolve the dependency for the last union
// type, so we are moving on to the next one. We hang onto
Expand All @@ -282,7 +310,7 @@ private function autowireDependency(ReflectionParameter $parameter, mixed $provi
throw $lastThrowable ?? new CannotAutowireException($this->chain, new Dependency($parameter));
}

private function autowireObjectDependency(ReflectionNamedType $type, mixed $providedValue): mixed
private function autowireObjectDependency(ReflectionNamedType $type, ?string $tag, mixed $providedValue): mixed
{
// If the provided value is of the right type,
// don't waste time autowiring, return it!
Expand All @@ -292,7 +320,7 @@ private function autowireObjectDependency(ReflectionNamedType $type, mixed $prov

// If we can successfully retrieve an instance
// of the necessary dependency, return it.
if ($instance = $this->resolve($type->getName())) {
if ($instance = $this->resolve(className: $type->getName(), tag: $tag)) {
return $instance;
}

Expand Down Expand Up @@ -361,4 +389,11 @@ public function __clone(): void
{
$this->chain = $this->chain?->clone();
}

private function resolveTaggedName(string $className, ?string $tag): string
{
return $tag
? "{$className}#{$tag}"
: $className;
}
}
15 changes: 15 additions & 0 deletions src/Container/Tag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Tempest\Container;

use Attribute;

#[Attribute]
final readonly class Tag
{
public function __construct(public string $name)
{
}
}
5 changes: 3 additions & 2 deletions src/Support/Reflection/Attributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
use ReflectionAttribute;
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;

/** @template T */
final class Attributes
{
private ReflectionClass|ReflectionMethod|ReflectionProperty $reflector;
private ReflectionClass|ReflectionMethod|ReflectionProperty|ReflectionParameter $reflector;

public function __construct(
private readonly string $attributeName,
Expand All @@ -33,7 +34,7 @@ public static function find(string $attributeName): self
* @param ReflectionClass|ReflectionMethod|ReflectionProperty $reflector
* @return $this<T>
*/
public function in(ReflectionClass|ReflectionMethod|ReflectionProperty|string $reflector): self
public function in(ReflectionClass|ReflectionMethod|ReflectionProperty|ReflectionParameter|string $reflector): self
{
if (is_string($reflector)) {
$reflector = new ReflectionClass($reflector);
Expand Down
Loading

0 comments on commit a81e28d

Please sign in to comment.