Skip to content

Commit

Permalink
Merge pull request #1041 from spiral/core/extended-injector
Browse files Browse the repository at this point in the history
Extended injector
  • Loading branch information
butschster authored Jan 3, 2024
2 parents 2fcc18b + b80910f commit 44459b0
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 89 deletions.
2 changes: 0 additions & 2 deletions src/Core/src/Attribute/Singleton.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace Spiral\Core\Attribute;

use Spiral\Core\Internal\Factory\Ctx;

/**
* Mark class as singleton.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Core/src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public function validateArguments(ContextFunction $reflection, array $arguments
* @throws ContainerException
* @throws \Throwable
*/
public function make(string $alias, array $parameters = [], string $context = null): mixed
public function make(string $alias, array $parameters = [], \Stringable|string|null $context = null): mixed
{
/** @psalm-suppress TooManyArguments */
return $this->factory->make($alias, $parameters, $context);
Expand Down
3 changes: 1 addition & 2 deletions src/Core/src/Container/InjectorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ interface InjectorInterface
* Parameter reflection can be used for dynamic class constructing, for example it can define
* database name or config section to be used to construct requested instance.
*
* @param \ReflectionClass $class Request class type.
* @param \ReflectionClass<TClass> $class Request class type.
* @param string|null $context Parameter or alias name.
* @psalm-assert \ReflectionClass<TClass> $class
*
* @return TClass
*
Expand Down
2 changes: 1 addition & 1 deletion src/Core/src/Internal/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function __construct(Registry $constructor)
* @throws ContainerException
* @throws \Throwable
*/
public function get(string|Autowire $id, string $context = null): mixed
public function get(string|Autowire $id, \Stringable|string|null $context = null): mixed
{
if ($id instanceof Autowire) {
return $id->resolve($this->factory);
Expand Down
116 changes: 71 additions & 45 deletions src/Core/src/Internal/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@

use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use ReflectionFunctionAbstract as ContextFunction;
use ReflectionParameter;
use Spiral\Core\Attribute\Finalize;
use Spiral\Core\Attribute\Scope as ScopeAttribute;
use Spiral\Core\Attribute\Singleton;
use Spiral\Core\BinderInterface;
use Spiral\Core\Config\Injectable;
use Spiral\Core\Config\DeferredFactory;
use Spiral\Core\Container\Autowire;
use Spiral\Core\Container\InjectorInterface;
use Spiral\Core\Container\SingletonInterface;
use Spiral\Core\Exception\Container\AutowireException;
Expand All @@ -29,6 +30,7 @@
use Spiral\Core\Internal\Factory\Ctx;
use Spiral\Core\InvokerInterface;
use Spiral\Core\ResolverInterface;
use Stringable;
use WeakReference;

/**
Expand Down Expand Up @@ -60,11 +62,11 @@ public function __construct(Registry $constructor)
}

/**
* @param string|null $context Related to parameter caused injection if any.
* @param Stringable|string|null $context Related to parameter caused injection if any.
*
* @throws \Throwable
*/
public function make(string $alias, array $parameters = [], string $context = null): mixed
public function make(string $alias, array $parameters = [], Stringable|string|null $context = null): mixed
{
if ($parameters === [] && \array_key_exists($alias, $this->state->singletons)) {
return $this->state->singletons[$alias];
Expand Down Expand Up @@ -94,7 +96,11 @@ public function make(string $alias, array $parameters = [], string $context = nu
DeferredFactory::class,
\Spiral\Core\Config\Factory::class => $this->resolveFactory($binding, $alias, $context, $parameters),
\Spiral\Core\Config\Shared::class => $this->resolveShared($binding, $alias, $context, $parameters),
Injectable::class => $this->resolveInjector($binding, $alias, $context, $parameters),
Injectable::class => $this->resolveInjector(
$binding,
new Ctx(alias: $alias, class: $alias, context: $context),
$parameters,
),
\Spiral\Core\Config\Scalar::class => $binding->value,
\Spiral\Core\Config\WeakReference::class => $this
->resolveWeakReference($binding, $alias, $context, $parameters),
Expand All @@ -107,22 +113,15 @@ public function make(string $alias, array $parameters = [], string $context = nu
}
}

private function resolveInjector(
Injectable $binding,
string $alias,
?string $context,
array $arguments,
) {
$ctx = new Ctx(alias: $alias, class: $alias, parameter: $context);

// We have to construct class using external injector when we know exact context
if ($arguments !== []) {
// todo factory?
}

$class = $ctx->class;
/**
* @psalm-suppress UnusedParam
* todo wat should we do with $arguments?
*/
private function resolveInjector(Injectable $binding, Ctx $ctx, array $arguments)
{
$context = $ctx->context;
try {
$ctx->reflection = $reflection = new \ReflectionClass($class);
$reflection = $ctx->reflection ??= new \ReflectionClass($ctx->class);
} catch (\ReflectionException $e) {
throw new ContainerException($e->getMessage(), $e->getCode(), $e);
}
Expand All @@ -142,10 +141,23 @@ private function resolveInjector(
);
}

/**
* @psalm-suppress RedundantCondition
*/
$instance = $injectorInstance->createInjection($reflection, $ctx->parameter);
/** @var array<class-string<InjectorInterface>, \ReflectionMethod|false> $cache reflection for extended injectors */
static $cache = [];
$extended = $cache[$injectorInstance::class] ??= (
static fn (\ReflectionType $type): bool =>
$type::class === \ReflectionUnionType::class || (string)$type === 'mixed'
)(
($refMethod = new \ReflectionMethod($injectorInstance, 'createInjection'))
->getParameters()[1]->getType()
) ? $refMethod : false;

$asIs = $extended && (\is_string($context) || $this->validateArguments($extended, [$reflection, $context]));
$instance = $injectorInstance->createInjection($reflection, match (true) {
$asIs => $context,
$context instanceof ReflectionParameter => $context->getName(),
default => (string)$context,
});

if (!$reflection->isInstance($instance)) {
throw new InjectionException(
\sprintf(
Expand All @@ -157,19 +169,19 @@ private function resolveInjector(

return $instance;
} finally {
$this->state->bindings[$reflection->getName()] ??= $binding; //new Injector($injector);
$this->state->bindings[$ctx->class] ??= $binding;
}
}

private function resolveAlias(
\Spiral\Core\Config\Alias $binding,
string $alias,
?string $context,
Stringable|string|null $context,
array $arguments,
): mixed {
$result = $binding->alias === $alias
? $this->autowire(
new Ctx(alias: $alias, class: $binding->alias, parameter: $context, singleton: $binding->singleton),
new Ctx(alias: $alias, class: $binding->alias, context: $context, singleton: $binding->singleton),
$arguments,
)
//Binding is pointing to something else
Expand All @@ -185,13 +197,13 @@ private function resolveAlias(
private function resolveShared(
\Spiral\Core\Config\Shared $binding,
string $alias,
?string $context,
Stringable|string|null $context,
array $arguments,
): object {
$avoidCache = $arguments !== [];
return $avoidCache
? $this->createInstance(
new Ctx(alias: $alias, class: $binding->value::class, parameter: $context),
new Ctx(alias: $alias, class: $binding->value::class, context: $context),
$arguments,
)
: $binding->value;
Expand All @@ -200,22 +212,22 @@ private function resolveShared(
private function resolveAutowire(
\Spiral\Core\Config\Autowire $binding,
string $alias,
?string $context,
Stringable|string|null $context,
array $arguments,
): mixed {
$instance = $binding->autowire->resolve($this, $arguments);

$ctx = new Ctx(alias: $alias, class: $alias, parameter: $context, singleton: $binding->singleton);
$ctx = new Ctx(alias: $alias, class: $alias, context: $context, singleton: $binding->singleton);
return $this->validateNewInstance($instance, $ctx, $arguments);
}

private function resolveFactory(
\Spiral\Core\Config\Factory|DeferredFactory $binding,
string $alias,
?string $context,
Stringable|string|null $context,
array $arguments,
): mixed {
$ctx = new Ctx(alias: $alias, class: $alias, parameter: $context, singleton: $binding->singleton);
$ctx = new Ctx(alias: $alias, class: $alias, context: $context, singleton: $binding->singleton);
try {
$instance = $binding::class === \Spiral\Core\Config\Factory::class && $binding->getParametersCount() === 0
? ($binding->factory)()
Expand All @@ -234,7 +246,7 @@ private function resolveFactory(
private function resolveWeakReference(
\Spiral\Core\Config\WeakReference $binding,
string $alias,
?string $context,
Stringable|string|null $context,
array $arguments,
): ?object {
$avoidCache = $arguments !== [];
Expand All @@ -244,7 +256,7 @@ private function resolveWeakReference(
$this->tracer->push(false, alias: $alias, source: WeakReference::class, context: $context);

$object = $this->createInstance(
new Ctx(alias: $alias, class: $alias, parameter: $context),
new Ctx(alias: $alias, class: $alias, context: $context),
$arguments,
);
if ($avoidCache) {
Expand All @@ -269,8 +281,11 @@ private function resolveWeakReference(
return $binding->reference->get();
}

private function resolveWithoutBinding(string $alias, array $parameters = [], string $context = null): mixed
{
private function resolveWithoutBinding(
string $alias,
array $parameters = [],
Stringable|string|null $context = null
): mixed {
$parent = $this->scope->getParent();

if ($parent !== null) {
Expand Down Expand Up @@ -302,7 +317,7 @@ private function resolveWithoutBinding(string $alias, array $parameters = [], st
try {
//No direct instructions how to construct class, make is automatically
return $this->autowire(
new Ctx(alias: $alias, class: $alias, parameter: $context),
new Ctx(alias: $alias, class: $alias, context: $context),
$parameters,
);
} finally {
Expand Down Expand Up @@ -370,7 +385,7 @@ private function validateNewInstance(
* @template TObject of object
*
* @param Ctx<TObject> $ctx
* @param array $parameters Constructor parameters.
* @param array $arguments Constructor arguments.
*
* @return TObject
*
Expand All @@ -379,7 +394,7 @@ private function validateNewInstance(
*/
private function createInstance(
Ctx $ctx,
array $parameters,
array $arguments,
): object {
$class = $ctx->class;
try {
Expand All @@ -394,9 +409,9 @@ private function createInstance(
throw new BadScopeException($scope, $class);
}

//We have to construct class using external injector when we know exact context
if ($parameters === [] && $this->binder->hasInjector($class)) {
return $this->resolveInjector($this->state->bindings[$ctx->class], $ctx->class, $ctx->parameter, $parameters);
// We have to construct class using external injector when we know the exact context
if ($arguments === [] && $this->binder->hasInjector($class)) {
return $this->resolveInjector($this->state->bindings[$ctx->class], $ctx, $arguments);
}

if (!$reflection->isInstantiable()) {
Expand All @@ -416,7 +431,7 @@ private function createInstance(
try {
$this->tracer->push(false, action: 'resolve arguments', signature: $constructor);
$this->tracer->push(true);
$arguments = $this->resolver->resolveArguments($constructor, $parameters);
$args = $this->resolver->resolveArguments($constructor, $arguments);
} catch (ValidationException $e) {
throw new ContainerException(
$this->tracer->combineTraceMessage(
Expand All @@ -433,9 +448,9 @@ private function createInstance(
}
try {
// Using constructor with resolved arguments
$this->tracer->push(false, call: "$class::__construct", arguments: $arguments);
$this->tracer->push(false, call: "$class::__construct", arguments: $args);
$this->tracer->push(true);
$instance = new $class(...$arguments);
$instance = new $class(...$args);
} catch (\TypeError $e) {
throw new WrongTypeException($constructor, $e);
} finally {
Expand Down Expand Up @@ -527,4 +542,15 @@ private function runInflector(object $instance): object

return $instance;
}

private function validateArguments(ContextFunction $reflection, array $arguments = []): bool
{
try {
$this->resolver->validateArguments($reflection, $arguments);
} catch (\Throwable) {
return false;
}

return true;
}
}
2 changes: 1 addition & 1 deletion src/Core/src/Internal/Factory/Ctx.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ final class Ctx
public function __construct(
public readonly string $alias,
public string $class,
public ?string $parameter = null,
public \Stringable|string|null $context = null,
public ?bool $singleton = null,
public ?\ReflectionClass $reflection = null,
) {
Expand Down
38 changes: 7 additions & 31 deletions src/Core/src/Internal/Resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ private function resolveParameter(ReflectionParameter $parameter, ResolvingState
$types = $reflectionType instanceof ReflectionNamedType ? [$reflectionType] : $reflectionType->getTypes();
foreach ($types as $namedType) {
try {
if ($this->resolveNamedType($state, $parameter, $namedType, $validate)) {
if (!$namedType->isBuiltin() && $this->resolveObject($state, $namedType, $parameter, $validate)) {
return true;
}
} catch (Throwable $e) {
Expand Down Expand Up @@ -237,45 +237,21 @@ private function resolveParameter(ReflectionParameter $parameter, ResolvingState
throw $error;
}

/**
* Resolve single named type. Returns {@see true} if argument was resolved.
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*
* @return bool
*/
private function resolveNamedType(
ResolvingState $state,
ReflectionParameter $parameter,
ReflectionNamedType $typeRef,
bool $validate,
) {
return !$typeRef->isBuiltin() && $this->resolveObjectParameter(
$state,
$typeRef->getName(),
$parameter->getName(),
$validate ? $parameter : null,
);
}

/**
* Resolve argument by class name and context. Returns {@see true} if argument resolved.
*
* @psalm-param class-string $class
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function resolveObjectParameter(
private function resolveObject(
ResolvingState $state,
string $class,
string $context,
ReflectionParameter $validateWith = null,
ReflectionNamedType $type,
ReflectionParameter $parameter,
bool $validateWith = false,
): bool {
/** @psalm-suppress TooManyArguments */
$argument = $this->container->get($class, $context);
$this->processArgument($state, $argument, $validateWith);
$argument = $this->container->get($type->getName(), $parameter);
$this->processArgument($state, $argument, $validateWith ? $parameter : null);
return true;
}

Expand Down
Loading

0 comments on commit 44459b0

Please sign in to comment.