From fc6fd7ce294f19d83b08ad73f1c15dae2986629e Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 19 Oct 2024 23:44:01 +0400 Subject: [PATCH 1/8] Add test cases --- src/Core/src/Config/Proxy.php | 1 + src/Core/tests/Scope/ProxyTest.php | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Core/src/Config/Proxy.php b/src/Core/src/Config/Proxy.php index 940b1f217..f9e9cb0d0 100644 --- a/src/Core/src/Config/Proxy.php +++ b/src/Core/src/Config/Proxy.php @@ -12,6 +12,7 @@ class Proxy extends Binding 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)); diff --git a/src/Core/tests/Scope/ProxyTest.php b/src/Core/tests/Scope/ProxyTest.php index fd2ddb43b..f7066bc93 100644 --- a/src/Core/tests/Scope/ProxyTest.php +++ b/src/Core/tests/Scope/ProxyTest.php @@ -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; @@ -304,7 +305,7 @@ public function __toString(): string public function testRecursiveProxy(): 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( @@ -336,6 +337,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() => new User('Foo'), + )); + + $name = $root->runScope( + new Scope(), + fn(#[Proxy] UserInterface $user) => $user->getName(), + ); + + self::assertSame('Foo', $name); + } + /* // Proxy::$attachContainer=true tests From d2b620442d8db59ddd9889e748c523fb4e198fe4 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 19 Oct 2024 23:49:52 +0400 Subject: [PATCH 2/8] Proxy config: add types --- src/Core/src/Config/Proxy.php | 12 ++++++++---- src/Core/tests/Scope/ProxyTest.php | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Core/src/Config/Proxy.php b/src/Core/src/Config/Proxy.php index f9e9cb0d0..4bfa4aaaf 100644 --- a/src/Core/src/Config/Proxy.php +++ b/src/Core/src/Config/Proxy.php @@ -4,19 +4,23 @@ namespace Spiral\Core\Config; +use Psr\Container\ContainerInterface; + class Proxy extends Binding { /** - * @param class-string $interface + * @template T + * @param class-string $interface + * @param null|\Closure(ContainerInterface): T $fallbackFactory */ 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.", + ); } public function __toString(): string diff --git a/src/Core/tests/Scope/ProxyTest.php b/src/Core/tests/Scope/ProxyTest.php index f7066bc93..d1d31d500 100644 --- a/src/Core/tests/Scope/ProxyTest.php +++ b/src/Core/tests/Scope/ProxyTest.php @@ -342,7 +342,7 @@ public function testProxyFallbackFactory() $root = new Container(); $root->bind(UserInterface::class, new ProxyConfig( interface: UserInterface::class, - fallbackFactory: static fn() => new User('Foo'), + fallbackFactory: static fn(): UserInterface => new User('Foo'), )); $name = $root->runScope( From 4bc94fca27bd408e2e79f530f963c58ea1e30b24 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 20 Oct 2024 00:36:32 +0400 Subject: [PATCH 3/8] Implement proxy fallback factory --- src/Core/src/Config/Proxy.php | 3 ++- .../Container/RecursiveProxyException.php | 10 +++++++ src/Core/src/Internal/Factory.php | 12 +++++++++ src/Core/src/Internal/Proxy/Resolver.php | 27 ++++++++++--------- src/Core/src/Internal/Proxy/RetryContext.php | 25 +++++++++++++++++ src/Core/tests/Scope/ProxyTest.php | 6 ++++- 6 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 src/Core/src/Internal/Proxy/RetryContext.php diff --git a/src/Core/src/Config/Proxy.php b/src/Core/src/Config/Proxy.php index 4bfa4aaaf..9cd3453e7 100644 --- a/src/Core/src/Config/Proxy.php +++ b/src/Core/src/Config/Proxy.php @@ -11,7 +11,8 @@ class Proxy extends Binding /** * @template T * @param class-string $interface - * @param null|\Closure(ContainerInterface): T $fallbackFactory + * @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, diff --git a/src/Core/src/Exception/Container/RecursiveProxyException.php b/src/Core/src/Exception/Container/RecursiveProxyException.php index 821975fca..3b813f575 100644 --- a/src/Core/src/Exception/Container/RecursiveProxyException.php +++ b/src/Core/src/Exception/Container/RecursiveProxyException.php @@ -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); + } } diff --git a/src/Core/src/Internal/Factory.php b/src/Core/src/Internal/Factory.php index eac3f5fe3..c5e05f06d 100644 --- a/src/Core/src/Internal/Factory.php +++ b/src/Core/src/Internal/Factory.php @@ -18,6 +18,7 @@ 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; @@ -25,6 +26,7 @@ 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; @@ -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) { @@ -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( diff --git a/src/Core/src/Internal/Proxy/Resolver.php b/src/Core/src/Internal/Proxy/Resolver.php index 2e0699594..6a1823342 100644 --- a/src/Core/src/Internal/Proxy/Resolver.php +++ b/src/Core/src/Internal/Proxy/Resolver.php @@ -34,27 +34,30 @@ 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)) { + /** + * If we get 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 { + return Proxy::isProxy($result) + ? $c->get($alias, new RetryContext($context)) + : $result; + } catch (RecursiveProxyException $e) { $scope = self::getScope($c); - throw new RecursiveProxyException( - $scope === null - ? "Recursive proxy detected for `{$alias}`." - : "Recursive proxy detected for `{$alias}` in `{$scope}` scope.", - ); + throw new RecursiveProxyException($e->alias, $e->bindingScope, $scope); } - - return $result; } /** * @return non-empty-string|null */ - private static function getScope(ContainerInterface $c): ?string + private static function getScope(ContainerInterface $c): ?array { if (!$c instanceof Container) { if (!Proxy::isProxy($c)) { @@ -64,9 +67,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), - ))); + )); } } diff --git a/src/Core/src/Internal/Proxy/RetryContext.php b/src/Core/src/Internal/Proxy/RetryContext.php new file mode 100644 index 000000000..c2dcdcef2 --- /dev/null +++ b/src/Core/src/Internal/Proxy/RetryContext.php @@ -0,0 +1,25 @@ +context; + } +} diff --git a/src/Core/tests/Scope/ProxyTest.php b/src/Core/tests/Scope/ProxyTest.php index d1d31d500..7a4504e41 100644 --- a/src/Core/tests/Scope/ProxyTest.php +++ b/src/Core/tests/Scope/ProxyTest.php @@ -309,7 +309,11 @@ public function testRecursiveProxy(): void $this->expectException(RecursiveProxyException::class); $this->expectExceptionMessage( - 'Recursive proxy detected for `Spiral\Tests\Core\Scope\Stub\UserInterface` in `root.null` scope.', + <<runScope( From 46c258d62eda179d0ffa80632fece23bbec870b2 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sat, 19 Oct 2024 20:36:53 +0000 Subject: [PATCH 4/8] Apply fixes from StyleCI [ci skip] [skip ci] --- src/Core/src/Internal/Proxy/RetryContext.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Core/src/Internal/Proxy/RetryContext.php b/src/Core/src/Internal/Proxy/RetryContext.php index c2dcdcef2..d99706c64 100644 --- a/src/Core/src/Internal/Proxy/RetryContext.php +++ b/src/Core/src/Internal/Proxy/RetryContext.php @@ -16,7 +16,8 @@ final class RetryContext implements \Stringable */ public function __construct( public \Stringable|string|null $context = null, - ) {} + ) { + } public function __toString(): string { From e0e2802a5db2d63733302fbdcc63afc173ce9508 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 20 Oct 2024 00:50:57 +0400 Subject: [PATCH 5/8] Add Proxy Fallback Factory to ServerRequestInterface in `http` scope --- src/Framework/Bootloader/Http/HttpBootloader.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Framework/Bootloader/Http/HttpBootloader.php b/src/Framework/Bootloader/Http/HttpBootloader.php index 4ce96cae1..2ac64f074 100644 --- a/src/Framework/Bootloader/Http/HttpBootloader.php +++ b/src/Framework/Bootloader/Http/HttpBootloader.php @@ -6,7 +6,7 @@ use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseFactoryInterface; -use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ServerRequestInterface as RequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Spiral\Boot\Bootloader\Bootloader; @@ -52,10 +52,13 @@ public function defineSingletons(): array $httpBinder->bindSingleton(Http::class, [self::class, 'httpCore']); $httpBinder->bindSingleton(CurrentRequest::class, CurrentRequest::class); $httpBinder->bind( - ServerRequestInterface::class, - static fn (CurrentRequest $request): ServerRequestInterface => $request->get() ?? throw new HttpException( - 'Unable to resolve current server request.', - ) + RequestInterface::class, + new \Spiral\Core\Config\Proxy( + interface: RequestInterface::class, + fallbackFactory: static fn (ContainerInterface $c): RequestInterface => $c + ->get(CurrentRequest::class) + ->get() ?? throw new HttpException('Unable to resolve the current server request.') + ), ); /** From 7e67bb4fb7220895687b334b390a59d7d4f29ad0 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 21 Oct 2024 17:35:57 +0400 Subject: [PATCH 6/8] Update bindings for Http Cookies and Sessions --- src/Cookies/src/CookieQueue.php | 7 ++++- src/Core/src/Config/Proxy.php | 3 ++ .../Shared/InvalidContainerScopeException.php | 3 +- src/Core/src/Internal/Proxy/Resolver.php | 19 +++++++------ .../Bootloader/Http/CookiesBootloader.php | 16 +++++------ .../InvalidRequestScopeException.php | 3 +- .../Bootloader/Http/HttpBootloader.php | 8 ++++-- .../Bootloader/Http/SessionBootloader.php | 28 +++++++++++++------ tests/Framework/Http/SessionTest.php | 9 ++++-- 9 files changed, 63 insertions(+), 33 deletions(-) diff --git a/src/Cookies/src/CookieQueue.php b/src/Cookies/src/CookieQueue.php index c6bfdaea5..85141f8d1 100644 --- a/src/Cookies/src/CookieQueue.php +++ b/src/Cookies/src/CookieQueue.php @@ -4,9 +4,14 @@ namespace Spiral\Cookies; +use Spiral\Cookies\Middleware\CookiesMiddleware; use Spiral\Core\Attribute\Scope; -#[Scope('http')] +/** + * @note The CookieQueue might be accessed in the http scope after the {@see CookiesMiddleware} has been executed, + * but don't store this class in stateful services, which are not isolated in the http-request scope. + */ +#[Scope('http-request')] final class CookieQueue { public const ATTRIBUTE = 'cookieQueue'; diff --git a/src/Core/src/Config/Proxy.php b/src/Core/src/Config/Proxy.php index 9cd3453e7..da90884ac 100644 --- a/src/Core/src/Config/Proxy.php +++ b/src/Core/src/Config/Proxy.php @@ -22,6 +22,9 @@ public function __construct( \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 diff --git a/src/Core/src/Exception/Shared/InvalidContainerScopeException.php b/src/Core/src/Exception/Shared/InvalidContainerScopeException.php index 48952d3e7..464db497e 100644 --- a/src/Core/src/Exception/Shared/InvalidContainerScopeException.php +++ b/src/Core/src/Exception/Shared/InvalidContainerScopeException.php @@ -18,6 +18,7 @@ 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 @@ -25,6 +26,6 @@ public function __construct( $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); } } diff --git a/src/Core/src/Internal/Proxy/Resolver.php b/src/Core/src/Internal/Proxy/Resolver.php index 6a1823342..01925890c 100644 --- a/src/Core/src/Internal/Proxy/Resolver.php +++ b/src/Core/src/Internal/Proxy/Resolver.php @@ -40,22 +40,23 @@ public static function resolve( } /** - * If we get a Proxy again, that we should retry with the new context + * 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 { - return Proxy::isProxy($result) - ? $c->get($alias, new RetryContext($context)) - : $result; - } catch (RecursiveProxyException $e) { - $scope = self::getScope($c); - throw new RecursiveProxyException($e->alias, $e->bindingScope, $scope); + if (Proxy::isProxy($result)) { + try { + return $c->get($alias, new RetryContext($context)); + } catch (RecursiveProxyException $e) { + throw new RecursiveProxyException($e->alias, $e->bindingScope, self::getScope($c)); + } } + + return $result; } /** - * @return non-empty-string|null + * @return list|null */ private static function getScope(ContainerInterface $c): ?array { diff --git a/src/Framework/Bootloader/Http/CookiesBootloader.php b/src/Framework/Bootloader/Http/CookiesBootloader.php index 0d3e1998d..f4f7d2b53 100644 --- a/src/Framework/Bootloader/Http/CookiesBootloader.php +++ b/src/Framework/Bootloader/Http/CookiesBootloader.php @@ -52,15 +52,15 @@ public function whitelistCookie(string $cookie): void $this->config->modify(CookiesConfig::CONFIG, new Append('excluded', null, $cookie)); } - private function cookieQueue(?ServerRequestInterface $request): CookieQueue + private function cookieQueue(ServerRequestInterface $request): CookieQueue { - if ($request === null) { - throw new InvalidRequestScopeException(CookieQueue::class); + try { + return $request->getAttribute(CookieQueue::ATTRIBUTE) ?? throw new ContextualObjectNotFoundException( + CookieQueue::class, + CookieQueue::ATTRIBUTE, + ); + } catch (InvalidRequestScopeException $e) { + throw new InvalidRequestScopeException(CookieQueue::class, previous: $e); } - - return $request->getAttribute(CookieQueue::ATTRIBUTE) ?? throw new ContextualObjectNotFoundException( - CookieQueue::class, - CookieQueue::ATTRIBUTE, - ); } } diff --git a/src/Framework/Bootloader/Http/Exception/InvalidRequestScopeException.php b/src/Framework/Bootloader/Http/Exception/InvalidRequestScopeException.php index 72adff19d..2f95ed2df 100644 --- a/src/Framework/Bootloader/Http/Exception/InvalidRequestScopeException.php +++ b/src/Framework/Bootloader/Http/Exception/InvalidRequestScopeException.php @@ -20,7 +20,8 @@ final class InvalidRequestScopeException extends InvalidContainerScopeException public function __construct( string $id, string|Container|null $scopeOrContainer = null, + \Throwable|null $previous = null, ) { - parent::__construct($id, $scopeOrContainer, Spiral::Http->value); + parent::__construct($id, $scopeOrContainer, Spiral::HttpRequest->value, $previous); } } diff --git a/src/Framework/Bootloader/Http/HttpBootloader.php b/src/Framework/Bootloader/Http/HttpBootloader.php index 2ac64f074..903d9e55a 100644 --- a/src/Framework/Bootloader/Http/HttpBootloader.php +++ b/src/Framework/Bootloader/Http/HttpBootloader.php @@ -10,17 +10,18 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Spiral\Boot\Bootloader\Bootloader; +use Spiral\Bootloader\Http\Exception\InvalidRequestScopeException; use Spiral\Config\ConfiguratorInterface; use Spiral\Config\Patch\Append; use Spiral\Core\Attribute\Proxy; use Spiral\Core\Attribute\Singleton; use Spiral\Core\BinderInterface; +use Spiral\Core\Container; use Spiral\Core\Container\Autowire; use Spiral\Core\InvokerInterface; use Spiral\Framework\Spiral; use Spiral\Http\Config\HttpConfig; use Spiral\Http\CurrentRequest; -use Spiral\Http\Exception\HttpException; use Spiral\Http\Http; use Spiral\Http\Pipeline; use Spiral\Telemetry\Bootloader\TelemetryBootloader; @@ -57,7 +58,10 @@ public function defineSingletons(): array interface: RequestInterface::class, fallbackFactory: static fn (ContainerInterface $c): RequestInterface => $c ->get(CurrentRequest::class) - ->get() ?? throw new HttpException('Unable to resolve the current server request.') + ->get() ?? throw new InvalidRequestScopeException( + RequestInterface::class, + $c instanceof Container ? $c : null, + ), ), ); diff --git a/src/Framework/Bootloader/Http/SessionBootloader.php b/src/Framework/Bootloader/Http/SessionBootloader.php index 4939d9ffe..39e5b7fe7 100644 --- a/src/Framework/Bootloader/Http/SessionBootloader.php +++ b/src/Framework/Bootloader/Http/SessionBootloader.php @@ -4,6 +4,7 @@ namespace Spiral\Bootloader\Http; +use Psr\Container\ContainerInterface; use Psr\Http\Message\ServerRequestInterface; use Spiral\Boot\Bootloader\Bootloader; use Spiral\Boot\DirectoriesInterface; @@ -30,18 +31,13 @@ public function __construct( public function defineBindings(): array { - $this->binder - ->getBinder(Spiral::Http) + $this->binder->getBinder(Spiral::HttpRequest)->bind(SessionInterface::class, $this->resolveSession(...)); + $this->binder->getBinder(Spiral::Http) ->bind( SessionInterface::class, - static fn (?ServerRequestInterface $request): SessionInterface => - ($request ?? throw new InvalidRequestScopeException(SessionInterface::class)) - ->getAttribute(SessionMiddleware::ATTRIBUTE) ?? throw new ContextualObjectNotFoundException( - SessionInterface::class, - SessionMiddleware::ATTRIBUTE, - ) + new Proxy(SessionInterface::class, false, $this->resolveSession(...)), ); - $this->binder->bind(SessionInterface::class, new Proxy(SessionInterface::class, false)); + $this->binder->bind(SessionInterface::class, new Proxy(SessionInterface::class, true),); return []; } @@ -91,4 +87,18 @@ public function boot( $cookies->whitelistCookie($session['cookie']); } + + private function resolveSession(ContainerInterface $container): SessionInterface + { + try { + /** @var ServerRequestInterface $request */ + $request = $container->get(ServerRequestInterface::class); + return $request->getAttribute(SessionMiddleware::ATTRIBUTE) ?? throw new ContextualObjectNotFoundException( + SessionInterface::class, + SessionMiddleware::ATTRIBUTE, + ); + } catch (InvalidRequestScopeException $e) { + throw new InvalidRequestScopeException(SessionInterface::class, previous: $e); + } + } } diff --git a/tests/Framework/Http/SessionTest.php b/tests/Framework/Http/SessionTest.php index 595ed40d7..ca60319d7 100644 --- a/tests/Framework/Http/SessionTest.php +++ b/tests/Framework/Http/SessionTest.php @@ -112,9 +112,11 @@ public function testInvalidSessionContextException(): void ])); $this->setHttpHandler(function (): void { + $session = $this->session(); + $this->expectException(ContextualObjectNotFoundException::class); - $this->session(); + $session->getID(); }); $this->fakeHttp()->get(uri: '/')->assertOk(); @@ -122,9 +124,12 @@ public function testInvalidSessionContextException(): void public function testSessionBindingWithoutRequest(): void { + // Get a Proxy + $session = $this->session(); + $this->expectException(InvalidRequestScopeException::class); - $this->session(); + $session->getID(); } private function session(): SessionInterface From 6bd377680922ecdd34060cb77f44b4b87e6d0b4b Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 21 Oct 2024 13:46:12 +0000 Subject: [PATCH 7/8] Apply fixes from StyleCI [ci skip] [skip ci] --- src/Framework/Bootloader/Http/SessionBootloader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Framework/Bootloader/Http/SessionBootloader.php b/src/Framework/Bootloader/Http/SessionBootloader.php index 39e5b7fe7..ac693c7e2 100644 --- a/src/Framework/Bootloader/Http/SessionBootloader.php +++ b/src/Framework/Bootloader/Http/SessionBootloader.php @@ -37,7 +37,7 @@ public function defineBindings(): array SessionInterface::class, new Proxy(SessionInterface::class, false, $this->resolveSession(...)), ); - $this->binder->bind(SessionInterface::class, new Proxy(SessionInterface::class, true),); + $this->binder->bind(SessionInterface::class, new Proxy(SessionInterface::class, true), ); return []; } From b6fe42959707294fdc774be94024118f8675ae55 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 21 Oct 2024 20:14:35 +0400 Subject: [PATCH 8/8] Detect Proxy recursion when the proxy was configured as singleton --- src/Core/src/Internal/Proxy/Resolver.php | 19 ++++++++++++------- src/Core/tests/Scope/ProxyTest.php | 24 +++++++++++++++++++++++- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/Core/src/Internal/Proxy/Resolver.php b/src/Core/src/Internal/Proxy/Resolver.php index 01925890c..98af51d18 100644 --- a/src/Core/src/Internal/Proxy/Resolver.php +++ b/src/Core/src/Internal/Proxy/Resolver.php @@ -39,20 +39,25 @@ public static function resolve( ); } + 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. */ - if (Proxy::isProxy($result)) { - try { - return $c->get($alias, new RetryContext($context)); - } catch (RecursiveProxyException $e) { - throw new RecursiveProxyException($e->alias, $e->bindingScope, self::getScope($c)); - } + try { + $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; } /** diff --git a/src/Core/tests/Scope/ProxyTest.php b/src/Core/tests/Scope/ProxyTest.php index 7a4504e41..7c94f1c2d 100644 --- a/src/Core/tests/Scope/ProxyTest.php +++ b/src/Core/tests/Scope/ProxyTest.php @@ -302,7 +302,7 @@ 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 ProxyConfig(UserInterface::class)); @@ -322,6 +322,28 @@ public function testRecursiveProxy(): void ); } + /** + * 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( + <<runScope( + new Scope(), + fn(#[Proxy] UserInterface $user) => $user->getName(), + ); + } + /** * The {@see ContainerScope::runScope} ignores Container Proxy to avoid recursion. */