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

[spiral/core] Advanced Context Handling in Injector Implementations #1041

Merged
merged 7 commits into from
Jan 3, 2024
Merged
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
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 @@
}

/**
* @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 @@
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 @@
}
}

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 @@
);
}

/**
* @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 @@

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 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 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 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 @@
$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 @@
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 @@
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 @@
* @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 createInstance(
Ctx $ctx,
array $parameters,
array $arguments,
): object {
$class = $ctx->class;
try {
Expand All @@ -394,9 +409,9 @@
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 @@
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 @@
}
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 @@

return $instance;
}

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

Check warning on line 551 in src/Core/src/Internal/Factory.php

View check run for this annotation

Codecov / codecov/patch

src/Core/src/Internal/Factory.php#L550-L551

Added lines #L550 - L551 were not covered by tests
}

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
Loading