Skip to content

Commit

Permalink
Merge hotfixes from 3.14.x
Browse files Browse the repository at this point in the history
  • Loading branch information
spiralbot committed Nov 24, 2024
1 parent c3d75eb commit 717059e
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 21 deletions.
17 changes: 13 additions & 4 deletions src/Config/Proxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,27 @@

namespace Spiral\Core\Config;

use Psr\Container\ContainerInterface;

class Proxy extends Binding
{
/**
* @param class-string $interface
* @template T
* @param class-string<T> $interface
* @param null|\Closure(ContainerInterface, \Stringable|string|null): T $fallbackFactory Factory that will be used
* to create an instance if the value is resolved from a proxy.
*/
public function __construct(
protected readonly string $interface,
public readonly bool $singleton = false,
public readonly ?\Closure $fallbackFactory = null,
) {
if (!\interface_exists($interface)) {
throw new \InvalidArgumentException(\sprintf('Interface `%s` does not exist.', $interface));
}
\interface_exists($interface) or throw new \InvalidArgumentException(
"Interface `{$interface}` does not exist.",
);
$this->singleton and $this->fallbackFactory !== null and throw new \InvalidArgumentException(
'Singleton proxies must not have a fallback factory.',
);
}

public function __toString(): string
Expand Down
10 changes: 10 additions & 0 deletions src/Exception/Container/RecursiveProxyException.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,14 @@
*/
class RecursiveProxyException extends ContainerException
{
public function __construct(
public readonly string $alias,
public readonly ?string $bindingScope = null,
public readonly ?array $callingScope = null,
) {
$message = "Recursive proxy detected for `$alias`.";
$bindingScope === null or $message .= "\nBinding scope: `$bindingScope`.";
$callingScope === null or $message .= "\nCalling scope: `" . \implode('.', $callingScope) . '`.';
parent::__construct($message);
}
}
3 changes: 2 additions & 1 deletion src/Exception/Shared/InvalidContainerScopeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ public function __construct(
protected readonly string $id,
Container|string|null $scopeOrContainer = null,
protected readonly ?string $requiredScope = null,
\Throwable|null $previous = null,
) {
$this->scope = \is_string($scopeOrContainer)
? $scopeOrContainer
: Introspector::scopeName($scopeOrContainer);

$req = $this->requiredScope !== null ? ", `$this->requiredScope` is required" : '';

parent::__construct("Unable to resolve `$id` in the `$this->scope` scope{$req}.");
parent::__construct("Unable to resolve `$id` in the `$this->scope` scope{$req}.", previous: $previous);
}
}
12 changes: 12 additions & 0 deletions src/Internal/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
use Spiral\Core\Exception\Container\InjectionException;
use Spiral\Core\Exception\Container\NotCallableException;
use Spiral\Core\Exception\Container\NotFoundException;
use Spiral\Core\Exception\Container\RecursiveProxyException;
use Spiral\Core\Exception\Resolver\ValidationException;
use Spiral\Core\Exception\Resolver\WrongTypeException;
use Spiral\Core\Exception\Scope\BadScopeException;
use Spiral\Core\FactoryInterface;
use Spiral\Core\Internal\Common\DestructorTrait;
use Spiral\Core\Internal\Common\Registry;
use Spiral\Core\Internal\Factory\Ctx;
use Spiral\Core\Internal\Proxy\RetryContext;
use Spiral\Core\InvokerInterface;
use Spiral\Core\Options;
use Spiral\Core\ResolverInterface;
Expand Down Expand Up @@ -198,6 +200,15 @@ private function resolveAlias(

private function resolveProxy(Config\Proxy $binding, string $alias, Stringable|string|null $context): mixed
{
if ($context instanceof RetryContext) {
return $binding->fallbackFactory === null
? throw new RecursiveProxyException(
$alias,
$this->scope->getScopeName(),
)
: ($binding->fallbackFactory)($this->container, $context->context);
}

$result = Proxy::create(new \ReflectionClass($binding->getInterface()), $context, new Attribute\Proxy());

if ($binding->singleton) {
Expand Down Expand Up @@ -316,6 +327,7 @@ private function resolveWithoutBinding(
} catch (ContainerExceptionInterface $e) {
$className = match (true) {
$e instanceof NotFoundException => NotFoundException::class,
$e instanceof RecursiveProxyException => throw $e,
default => ContainerException::class,
};
throw new $className($this->tracer->combineTraceMessage(\sprintf(
Expand Down
36 changes: 23 additions & 13 deletions src/Internal/Proxy/Resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,37 @@ public static function resolve(
throw new ContainerException(
$scope === null
? "Unable to resolve `{$alias}` in a Proxy."
: "Unable to resolve `{$alias}` in a Proxy in `{$scope}` scope.",
: \sprintf('Unable to resolve `%s` in a Proxy in `%s` scope.', $alias, \implode('.', $scope)),
previous: $e,
);
}

if (Proxy::isProxy($result)) {
$scope = self::getScope($c);
throw new RecursiveProxyException(
$scope === null
? "Recursive proxy detected for `{$alias}`."
: "Recursive proxy detected for `{$alias}` in `{$scope}` scope.",
);
if (!Proxy::isProxy($result)) {
return $result;
}

/**
* If we got a Proxy again, that we should retry with the new context
* to try to get the instance from the Proxy Fallback Factory.
* If there is no the Proxy Fallback Factory, {@see RecursiveProxyException} will be thrown.
*/
try {
/** @psalm-suppress TooManyArguments */
$result = $c->get($alias, new RetryContext($context));
} catch (RecursiveProxyException $e) {
throw new RecursiveProxyException($e->alias, $e->bindingScope, self::getScope($c));
}

return $result;
// If Container returned a Proxy after the retry, then we have a recursion.
return Proxy::isProxy($result)
? throw new RecursiveProxyException($alias, null, self::getScope($c))
: $result;
}

/**
* @return non-empty-string|null
* @return list<non-empty-string|null>|null
*/
private static function getScope(ContainerInterface $c): ?string
private static function getScope(ContainerInterface $c): ?array
{
if (!$c instanceof Container) {
if (!Proxy::isProxy($c)) {
Expand All @@ -64,9 +74,9 @@ private static function getScope(ContainerInterface $c): ?string
$c = null;
}

return \implode('.', \array_reverse(\array_map(
return \array_reverse(\array_map(
static fn (?string $name): string => $name ?? 'null',
Introspector::scopeNames($c),
)));
));
}
}
26 changes: 26 additions & 0 deletions src/Internal/Proxy/RetryContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Spiral\Core\Internal\Proxy;

/**
* Used to wrap the resolving context to force Proxy Fallback Factory.
*
* @internal
*/
final class RetryContext implements \Stringable
{
/**
* @param \Stringable|string|null $context Original context.
*/
public function __construct(
public \Stringable|string|null $context = null,
) {
}

public function __toString(): string
{
return (string) $this->context;
}
}
49 changes: 46 additions & 3 deletions tests/Scope/ProxyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Psr\Container\ContainerInterface;
use ReflectionParameter;
use Spiral\Core\Attribute\Proxy;
use Spiral\Core\Config\Proxy as ProxyConfig;
use Spiral\Core\Container;
use Spiral\Core\Container\InjectorInterface;
use Spiral\Core\ContainerScope;
Expand Down Expand Up @@ -301,14 +302,40 @@ public function __toString(): string
/**
* Proxy gets a proxy of the same type.
*/
public function testRecursiveProxy(): void
public function testRecursiveProxyNotSingleton(): void
{
$root = new Container();
$root->bind(UserInterface::class, new \Spiral\Core\Config\Proxy(UserInterface::class));
$root->bind(UserInterface::class, new ProxyConfig(UserInterface::class));

$this->expectException(RecursiveProxyException::class);
$this->expectExceptionMessage(
'Recursive proxy detected for `Spiral\Tests\Core\Scope\Stub\UserInterface` in `root.null` scope.',
<<<MSG
Recursive proxy detected for `Spiral\Tests\Core\Scope\Stub\UserInterface`.
Binding scope: `root`.
Calling scope: `root.null`.
MSG,
);

$root->runScope(
new Scope(),
fn(#[Proxy] UserInterface $user) => $user->getName(),
);
}

/**
* Proxy gets a proxy of the same type as a singleton.
*/
public function testRecursiveProxySingleton(): void
{
$root = new Container();
$root->bind(UserInterface::class, new ProxyConfig(UserInterface::class, singleton: true));

$this->expectException(RecursiveProxyException::class);
$this->expectExceptionMessage(
<<<MSG
Recursive proxy detected for `Spiral\Tests\Core\Scope\Stub\UserInterface`.
Calling scope: `root.null`.
MSG,
);

$root->runScope(
Expand Down Expand Up @@ -336,6 +363,22 @@ static function (#[Proxy] ContainerInterface $proxy, ContainerInterface $scoped)
);
}

public function testProxyFallbackFactory()
{
$root = new Container();
$root->bind(UserInterface::class, new ProxyConfig(
interface: UserInterface::class,
fallbackFactory: static fn(): UserInterface => new User('Foo'),
));

$name = $root->runScope(
new Scope(),
fn(#[Proxy] UserInterface $user) => $user->getName(),
);

self::assertSame('Foo', $name);
}

/*
// Proxy::$attachContainer=true tests
Expand Down

0 comments on commit 717059e

Please sign in to comment.