diff --git a/src/AuthHttp/src/Middleware/AuthMiddleware.php b/src/AuthHttp/src/Middleware/AuthMiddleware.php index 73c5898bb..5d779a863 100644 --- a/src/AuthHttp/src/Middleware/AuthMiddleware.php +++ b/src/AuthHttp/src/Middleware/AuthMiddleware.php @@ -22,13 +22,17 @@ final class AuthMiddleware implements MiddlewareInterface { public const ATTRIBUTE = 'authContext'; + public const TOKEN_STORAGE_ATTRIBUTE = 'tokenStorage'; + /** + * @param ScopeInterface $scope Deprecated, will be removed in v4.0. + */ public function __construct( private readonly ScopeInterface $scope, private readonly ActorProviderInterface $actorProvider, private readonly TokenStorageInterface $tokenStorage, private readonly TransportRegistry $transportRegistry, - private readonly ?EventDispatcherInterface $eventDispatcher = null + private readonly ?EventDispatcherInterface $eventDispatcher = null, ) { } @@ -39,12 +43,10 @@ public function process(Request $request, RequestHandlerInterface $handler): Res { $authContext = $this->initContext($request, new AuthContext($this->actorProvider, $this->eventDispatcher)); - $response = $this->scope->runScope( - [ - AuthContextInterface::class => $authContext, - TokenStorageInterface::class => $this->tokenStorage, - ], - static fn () => $handler->handle($request->withAttribute(self::ATTRIBUTE, $authContext)) + $response = $handler->handle( + $request + ->withAttribute(self::ATTRIBUTE, $authContext) + ->withAttribute(self::TOKEN_STORAGE_ATTRIBUTE, $this->tokenStorage), ); return $this->closeContext($request, $response, $authContext); @@ -85,7 +87,7 @@ private function closeContext(Request $request, Response $response, AuthContextI return $transport->removeToken( $request, $response, - $authContext->getToken()->getID() + $authContext->getToken()->getID(), ); } @@ -93,7 +95,7 @@ private function closeContext(Request $request, Response $response, AuthContextI $request, $response, $authContext->getToken()->getID(), - $authContext->getToken()->getExpiresAt() + $authContext->getToken()->getExpiresAt(), ); } } diff --git a/src/AuthHttp/src/Middleware/AuthTransportMiddleware.php b/src/AuthHttp/src/Middleware/AuthTransportMiddleware.php index c64576732..603f085e2 100644 --- a/src/AuthHttp/src/Middleware/AuthTransportMiddleware.php +++ b/src/AuthHttp/src/Middleware/AuthTransportMiddleware.php @@ -21,6 +21,9 @@ final class AuthTransportMiddleware implements MiddlewareInterface { private readonly AuthMiddleware $authMiddleware; + /** + * @param ScopeInterface $scope Deprecated, will be removed in v4.0. + */ public function __construct( string $transportName, ScopeInterface $scope, diff --git a/src/AuthHttp/src/Middleware/AuthTransportWithStorageMiddleware.php b/src/AuthHttp/src/Middleware/AuthTransportWithStorageMiddleware.php index 92281a010..b92363eef 100644 --- a/src/AuthHttp/src/Middleware/AuthTransportWithStorageMiddleware.php +++ b/src/AuthHttp/src/Middleware/AuthTransportWithStorageMiddleware.php @@ -21,6 +21,9 @@ final class AuthTransportWithStorageMiddleware implements MiddlewareInterface { private readonly MiddlewareInterface $authMiddleware; + /** + * @param ScopeInterface $scope Deprecated, will be removed in v4.0. + */ public function __construct( string $transportName, ScopeInterface $scope, diff --git a/src/AuthHttp/tests/AuthTransportWithStorageMiddlewareTest.php b/src/AuthHttp/tests/AuthTransportWithStorageMiddlewareTest.php index 30f5a41c3..670b5a070 100644 --- a/src/AuthHttp/tests/AuthTransportWithStorageMiddlewareTest.php +++ b/src/AuthHttp/tests/AuthTransportWithStorageMiddlewareTest.php @@ -4,8 +4,6 @@ namespace Spiral\Tests\Auth; -use Mockery as m; -use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -19,54 +17,63 @@ use Spiral\Auth\TransportRegistry; use Spiral\Core\ScopeInterface; -final class AuthTransportWithStorageMiddlewareTest extends TestCase +final class AuthTransportWithStorageMiddlewareTest extends BaseTestCase { public function testProcessMiddlewareWithTokenStorageProvider(): void { - $serverRequest = m::mock(ServerRequestInterface::class); - $storageProvider = m::mock(TokenStorageProviderInterface::class); - $scope = m::mock(ScopeInterface::class); - $response = m::mock(ResponseInterface::class); + $storageProvider = $this->createMock(TokenStorageProviderInterface::class); + $storageProvider + ->expects($this->once()) + ->method('getStorage') + ->with('session') + ->willReturn($tokenStorage = $this->createMock(TokenStorageInterface::class)); - $storageProvider->shouldReceive('getStorage')->once()->with('session')->andReturn( - $tokenStorage = m::mock(TokenStorageInterface::class) - ); + $matcher = $this->exactly(2); + $request = $this->createMock(ServerRequestInterface::class); + $request + ->expects($this->exactly(2)) + ->method('withAttribute') + ->willReturnCallback(function (string $key, string $value) use ($matcher, $tokenStorage) { + match ($matcher->numberOfInvocations()) { + 1 => $this->assertInstanceOf(AuthContextInterface::class, $value), + 2 => $this->assertSame($tokenStorage, $value), + }; + }) + ->willReturnSelf(); + + $response = $this->createMock(ResponseInterface::class); $registry = new TransportRegistry(); - $registry->setTransport('header', $transport = m::mock(HttpTransportInterface::class)); + $registry->setTransport('header', $transport = $this->createMock(HttpTransportInterface::class)); - $transport->shouldReceive('fetchToken')->once()->with($serverRequest)->andReturn('fooToken'); - $transport->shouldReceive('commitToken')->once()->with($serverRequest, $response, '123', null) - ->andReturn($response); + $transport->expects($this->once())->method('fetchToken')->with($request)->willReturn('fooToken'); + $transport->expects($this->once())->method('commitToken')->with($request, $response, '123', null) + ->willReturn($response); - $tokenStorage->shouldReceive('load')->once()->with('fooToken')->andReturn( - $token = m::mock(TokenInterface::class) - ); + $tokenStorage + ->expects($this->once()) + ->method('load') + ->with('fooToken') + ->willReturn($token = $this->createMock(TokenInterface::class)); - $scope - ->shouldReceive('runScope') - ->once() - ->withArgs( - fn(array $bindings, callable $callback) => $bindings[AuthContextInterface::class] - ->getToken() instanceof $token - ) - ->andReturn($response); - - $token->shouldReceive('getID')->once()->andReturn('123'); - $token->shouldReceive('getExpiresAt')->once()->andReturnNull(); + $token->expects($this->once())->method('getID')->willReturn('123'); + $token->expects($this->once())->method('getExpiresAt')->willReturn(null); $middleware = new AuthTransportWithStorageMiddleware( 'header', - $scope, - m::mock(ActorProviderInterface::class), + $this->createMock(ScopeInterface::class), + $this->createMock(ActorProviderInterface::class), $storageProvider, $registry, storage: 'session' ); + $handler = $this->createMock(RequestHandlerInterface::class); + $handler->expects($this->once())->method('handle')->with($request)->willReturn($response); + $this->assertSame( $response, - $middleware->process($serverRequest, m::mock(RequestHandlerInterface::class)) + $middleware->process($request, $handler) ); } } diff --git a/src/AuthHttp/tests/BaseTestCase.php b/src/AuthHttp/tests/BaseTestCase.php index e35fff70b..fac77fa7d 100644 --- a/src/AuthHttp/tests/BaseTestCase.php +++ b/src/AuthHttp/tests/BaseTestCase.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use Spiral\Core\Container; +use Spiral\Core\Options; use Spiral\Telemetry\NullTracer; use Spiral\Telemetry\TracerInterface; @@ -15,7 +16,9 @@ abstract class BaseTestCase extends TestCase protected function setUp(): void { - $this->container = new Container(); + $options = new Options(); + $options->checkScope = false; + $this->container = new Container(options: $options); $this->container->bind( TracerInterface::class, diff --git a/src/AuthHttp/tests/CookieTransportTest.php b/src/AuthHttp/tests/CookieTransportTest.php index 86fcc88d9..0409cd4c9 100644 --- a/src/AuthHttp/tests/CookieTransportTest.php +++ b/src/AuthHttp/tests/CookieTransportTest.php @@ -20,7 +20,7 @@ use Spiral\Tests\Auth\Stub\TestAuthHttpStorage; use Spiral\Tests\Auth\Stub\TestAuthHttpToken; -class CookieTransportTest extends BaseTestCase +final class CookieTransportTest extends BaseTestCase { public function testCookieToken(): void { diff --git a/src/AuthHttp/tests/HeaderTransportTest.php b/src/AuthHttp/tests/HeaderTransportTest.php index e12702206..3a05ccbee 100644 --- a/src/AuthHttp/tests/HeaderTransportTest.php +++ b/src/AuthHttp/tests/HeaderTransportTest.php @@ -4,41 +4,28 @@ namespace Spiral\Tests\Auth; -use PHPUnit\Framework\TestCase; -use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Spiral\Auth\HttpTransportInterface; use Spiral\Auth\Middleware\AuthMiddleware; use Spiral\Auth\Transport\HeaderTransport; use Spiral\Auth\TransportRegistry; -use Spiral\Core\Container; use Spiral\Http\Config\HttpConfig; use Spiral\Http\Http; use Spiral\Http\Pipeline; -use Spiral\Telemetry\NullTracer; -use Spiral\Telemetry\TracerInterface; use Spiral\Tests\Auth\Diactoros\ResponseFactory; use Nyholm\Psr7\ServerRequest; use Spiral\Tests\Auth\Stub\TestAuthHttpProvider; use Spiral\Tests\Auth\Stub\TestAuthHttpStorage; use Spiral\Tests\Auth\Stub\TestAuthHttpToken; -class HeaderTransportTest extends TestCase +final class HeaderTransportTest extends BaseTestCase { - private Container $container; - - public function setUp(): void - { - $this->container = new Container(); - $this->container->bind(TracerInterface::class, new NullTracer($this->container)); - } - public function testHeaderToken(): void { $http = $this->getCore(new HeaderTransport()); $http->setHandler( - static function (ServerRequestInterface $request, ResponseInterface $response): void { + static function (ServerRequestInterface $request): void { if ($request->getAttribute('authContext')->getToken() === null) { echo 'no token'; } else { @@ -62,7 +49,7 @@ public function testHeaderTokenWithCustomValueFormat(): void $http = $this->getCore(new HeaderTransport('Authorization', 'Bearer %s')); $http->setHandler( - static function (ServerRequestInterface $request, ResponseInterface $response): void { + static function (ServerRequestInterface $request): void { if ($request->getAttribute('authContext')->getToken() === null) { echo 'no token'; } else { @@ -85,7 +72,7 @@ public function testBadHeaderToken(): void $http = $this->getCore(new HeaderTransport()); $http->setHandler( - static function (ServerRequestInterface $request, ResponseInterface $response): void { + static function (ServerRequestInterface $request): void { if ($request->getAttribute('authContext')->getToken() === null) { echo 'no token'; } else { @@ -109,7 +96,7 @@ public function testDeleteToken(): void $http = $this->getCore(new HeaderTransport()); $http->setHandler( - static function (ServerRequestInterface $request, ResponseInterface $response): void { + static function (ServerRequestInterface $request): void { $request->getAttribute('authContext')->close(); echo 'closed'; } @@ -127,7 +114,7 @@ public function testCommitToken(): void $http = $this->getCore(new HeaderTransport()); $http->setHandler( - static function (ServerRequestInterface $request, ResponseInterface $response): void { + static function (ServerRequestInterface $request): void { $request->getAttribute('authContext')->start( new TestAuthHttpToken('new-token', ['ok' => 1]) ); @@ -144,7 +131,7 @@ public function testCommitTokenWithCustomValueFormat(): void $http = $this->getCore(new HeaderTransport('Authorization', 'Bearer %s')); $http->setHandler( - static function (ServerRequestInterface $request, ResponseInterface $response): void { + static function (ServerRequestInterface $request): void { $request->getAttribute('authContext')->start( new TestAuthHttpToken('new-token', ['ok' => 1]) ); diff --git a/src/AuthHttp/tests/Middleware/AuthMiddlewareTest.php b/src/AuthHttp/tests/Middleware/AuthMiddlewareTest.php index 8031bbf1f..efc22c800 100644 --- a/src/AuthHttp/tests/Middleware/AuthMiddlewareTest.php +++ b/src/AuthHttp/tests/Middleware/AuthMiddlewareTest.php @@ -18,7 +18,7 @@ use Spiral\Tests\Auth\Stub\TestAuthHttpProvider; use Spiral\Tests\Auth\Stub\TestAuthHttpStorage; -class AuthMiddlewareTest extends BaseTestCase +final class AuthMiddlewareTest extends BaseTestCase { public function testAttributeRead(): void { diff --git a/src/Cookies/src/CookieQueue.php b/src/Cookies/src/CookieQueue.php index 7b83fab91..c6bfdaea5 100644 --- a/src/Cookies/src/CookieQueue.php +++ b/src/Cookies/src/CookieQueue.php @@ -4,6 +4,9 @@ namespace Spiral\Cookies; +use Spiral\Core\Attribute\Scope; + +#[Scope('http')] final class CookieQueue { public const ATTRIBUTE = 'cookieQueue'; diff --git a/src/Cookies/tests/CookiesTest.php b/src/Cookies/tests/CookiesTest.php index 4834e330b..c87fc0d91 100644 --- a/src/Cookies/tests/CookiesTest.php +++ b/src/Cookies/tests/CookiesTest.php @@ -12,7 +12,7 @@ use Spiral\Cookies\CookieQueue; use Spiral\Cookies\Middleware\CookiesMiddleware; use Spiral\Core\Container; -use Spiral\Core\ContainerScope; +use Spiral\Core\Options; use Spiral\Encrypter\Config\EncrypterConfig; use Spiral\Encrypter\Encrypter; use Spiral\Encrypter\EncrypterFactory; @@ -22,17 +22,16 @@ use Spiral\Http\Http; use Spiral\Http\Pipeline; use Nyholm\Psr7\ServerRequest; -use Spiral\Telemetry\NullTracer; -use Spiral\Telemetry\TracerInterface; -class CookiesTest extends TestCase +final class CookiesTest extends TestCase { - private $container; + private Container $container; public function setUp(): void { - $this->container = new Container(); - $this->container->bind(TracerInterface::class, new NullTracer($this->container)); + $options = new Options(); + $options->checkScope = false; + $this->container = new Container(options: $options); $this->container->bind(CookiesConfig::class, new CookiesConfig([ 'domain' => '.%s', 'method' => CookiesConfig::COOKIE_ENCRYPT, @@ -50,22 +49,11 @@ public function setUp(): void $this->container->bind(EncrypterInterface::class, Encrypter::class); } - public function testScope(): void + public function testCookieQueueInRequestAttribute(): void { $core = $this->httpCore([CookiesMiddleware::class]); - $core->setHandler(function ($r) { - $this->assertInstanceOf( - CookieQueue::class, - ContainerScope::getContainer()->get(ServerRequestInterface::class) - ->getAttribute(CookieQueue::ATTRIBUTE) - ); - - $this->assertSame( - ContainerScope::getContainer()->get(ServerRequestInterface::class) - ->getAttribute(CookieQueue::ATTRIBUTE), - $r->getAttribute(CookieQueue::ATTRIBUTE) - ); - + $core->setHandler(function (ServerRequestInterface $r) { + $this->assertInstanceOf(CookieQueue::class, $r->getAttribute(CookieQueue::ATTRIBUTE)); return 'all good'; }); @@ -77,9 +65,8 @@ public function testScope(): void public function testSetEncryptedCookie(): void { $core = $this->httpCore([CookiesMiddleware::class]); - $core->setHandler(function ($r) { - ContainerScope::getContainer()->get(ServerRequestInterface::class) - ->getAttribute(CookieQueue::ATTRIBUTE)->set('name', 'value'); + $core->setHandler(function (ServerRequestInterface $r) { + $r->getAttribute(CookieQueue::ATTRIBUTE)->set('name', 'value'); return 'all good'; }); @@ -99,9 +86,8 @@ public function testSetEncryptedCookie(): void public function testSetNotProtectedCookie(): void { $core = $this->httpCore([CookiesMiddleware::class]); - $core->setHandler(function ($r) { - ContainerScope::getContainer()->get(ServerRequestInterface::class) - ->getAttribute(CookieQueue::ATTRIBUTE)->set('PHPSESSID', 'value'); + $core->setHandler(function (ServerRequestInterface $r) { + $r->getAttribute(CookieQueue::ATTRIBUTE)->set('PHPSESSID', 'value'); return 'all good'; }); @@ -118,11 +104,7 @@ public function testSetNotProtectedCookie(): void public function testDecrypt(): void { $core = $this->httpCore([CookiesMiddleware::class]); - $core->setHandler(function ($r) { - - /** - * @var ServerRequest $r - */ + $core->setHandler(function (ServerRequestInterface $r) { return $r->getCookieParams()['name']; }); @@ -136,11 +118,7 @@ public function testDecrypt(): void public function testDecryptArray(): void { $core = $this->httpCore([CookiesMiddleware::class]); - $core->setHandler(function ($r) { - - /** - * @var ServerRequest $r - */ + $core->setHandler(function (ServerRequestInterface $r) { return $r->getCookieParams()['name'][0]; }); @@ -154,11 +132,7 @@ public function testDecryptArray(): void public function testDecryptBroken(): void { $core = $this->httpCore([CookiesMiddleware::class]); - $core->setHandler(function ($r) { - - /** - * @var ServerRequest $r - */ + $core->setHandler(function (ServerRequestInterface $r) { return $r->getCookieParams()['name']; }); @@ -172,12 +146,9 @@ public function testDecryptBroken(): void public function testDelete(): void { $core = $this->httpCore([CookiesMiddleware::class]); - $core->setHandler(function ($r) { - ContainerScope::getContainer()->get(ServerRequestInterface::class) - ->getAttribute(CookieQueue::ATTRIBUTE)->set('name', 'value'); - - ContainerScope::getContainer()->get(ServerRequestInterface::class) - ->getAttribute(CookieQueue::ATTRIBUTE)->delete('name'); + $core->setHandler(function (ServerRequestInterface $r) { + $r->getAttribute(CookieQueue::ATTRIBUTE)->set('name', 'value'); + $r->getAttribute(CookieQueue::ATTRIBUTE)->delete('name'); return 'all good'; }); @@ -200,9 +171,8 @@ public function testUnprotected(): void ])); $core = $this->httpCore([CookiesMiddleware::class]); - $core->setHandler(function ($r) { - ContainerScope::getContainer()->get(ServerRequestInterface::class) - ->getAttribute(CookieQueue::ATTRIBUTE)->set('name', 'value'); + $core->setHandler(function (ServerRequestInterface $r) { + $r->getAttribute(CookieQueue::ATTRIBUTE)->set('name', 'value'); return 'all good'; }); @@ -225,11 +195,7 @@ public function testGetUnprotected(): void ])); $core = $this->httpCore([CookiesMiddleware::class]); - $core->setHandler(function ($r) { - - /** - * @var ServerRequest $r - */ + $core->setHandler(function (ServerRequestInterface $r) { return $r->getCookieParams()['name']; }); @@ -249,9 +215,8 @@ public function testHMAC(): void ])); $core = $this->httpCore([CookiesMiddleware::class]); - $core->setHandler(function ($r) { - ContainerScope::getContainer()->get(ServerRequestInterface::class) - ->getAttribute(CookieQueue::ATTRIBUTE)->set('name', 'value'); + $core->setHandler(function (ServerRequestInterface $r) { + $r->getAttribute(CookieQueue::ATTRIBUTE)->set('name', 'value'); return 'all good'; }); @@ -272,7 +237,7 @@ public function testHMAC(): void $this->assertSame('value', (string)$response->getBody()); } - protected function httpCore(array $middleware = []): Http + private function httpCore(array $middleware = []): Http { $config = new HttpConfig([ 'basePath' => '/', @@ -290,9 +255,9 @@ protected function httpCore(array $middleware = []): Http ); } - protected function get( + private function get( Http $core, - $uri, + string $uri, array $query = [], array $headers = [], array $cookies = [] @@ -300,8 +265,8 @@ protected function get( return $core->handle($this->request($uri, 'GET', $query, $headers, $cookies)); } - protected function request( - $uri, + private function request( + string $uri, string $method, array $query = [], array $headers = [], @@ -314,7 +279,7 @@ protected function request( ->withCookieParams($cookies); } - protected function fetchCookies(ResponseInterface $response) + private function fetchCookies(ResponseInterface $response): array { $result = []; diff --git a/src/Core/src/Exception/Shared/InvalidContainerScopeException.php b/src/Core/src/Exception/Shared/InvalidContainerScopeException.php new file mode 100644 index 000000000..48952d3e7 --- /dev/null +++ b/src/Core/src/Exception/Shared/InvalidContainerScopeException.php @@ -0,0 +1,30 @@ +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}."); + } +} diff --git a/src/Csrf/tests/CsrfTest.php b/src/Csrf/tests/CsrfTest.php index 862b37374..9888cab99 100644 --- a/src/Csrf/tests/CsrfTest.php +++ b/src/Csrf/tests/CsrfTest.php @@ -8,6 +8,7 @@ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Spiral\Core\Container; +use Spiral\Core\Options; use Spiral\Csrf\Config\CsrfConfig; use Spiral\Csrf\Middleware\CsrfFirewall; use Spiral\Csrf\Middleware\CsrfMiddleware; @@ -19,13 +20,15 @@ use Spiral\Telemetry\NullTracer; use Spiral\Telemetry\TracerInterface; -class CsrfTest extends TestCase +final class CsrfTest extends TestCase { private Container $container; public function setUp(): void { - $this->container = new Container(); + $options = new Options(); + $options->checkScope = false; + $this->container = new Container(options: $options); $this->container->bind( CsrfConfig::class, new CsrfConfig( @@ -37,11 +40,7 @@ public function setUp(): void ) ); - $this->container->bind( - TracerInterface::class, - new NullTracer($this->container) - ); - + $this->container->bind(TracerInterface::class, new NullTracer($this->container)); $this->container->bind( ResponseFactoryInterface::class, new TestResponseFactory(new HttpConfig(['headers' => []])) @@ -214,7 +213,7 @@ static function () { self::assertSame('all good', (string)$response->getBody()); } - protected function httpCore(array $middleware = []): Http + private function httpCore(array $middleware = []): Http { $config = new HttpConfig( [ @@ -234,9 +233,9 @@ protected function httpCore(array $middleware = []): Http ); } - protected function get( + private function get( Http $core, - $uri, + string $uri, array $query = [], array $headers = [], array $cookies = [] @@ -244,9 +243,9 @@ protected function get( return $core->handle($this->request($uri, 'GET', $query, $headers, $cookies)); } - protected function post( + private function post( Http $core, - $uri, + string $uri, array $data = [], array $headers = [], array $cookies = [] @@ -254,8 +253,8 @@ protected function post( return $core->handle($this->request($uri, 'POST', [], $headers, $cookies)->withParsedBody($data)); } - protected function request( - $uri, + private function request( + string $uri, string $method, array $query = [], array $headers = [], @@ -268,7 +267,7 @@ protected function request( ->withCookieParams($cookies); } - protected function fetchCookies(ResponseInterface $response): array + private function fetchCookies(ResponseInterface $response): array { $result = []; diff --git a/src/Filters/tests/BaseTestCase.php b/src/Filters/tests/BaseTestCase.php index 4b6ab9695..adbf765b9 100644 --- a/src/Filters/tests/BaseTestCase.php +++ b/src/Filters/tests/BaseTestCase.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use Spiral\Core\Container; +use Spiral\Core\Options; use Spiral\Validation\ValidationInterface; use Spiral\Validation\ValidationProvider; @@ -16,7 +17,9 @@ abstract class BaseTestCase extends TestCase public function setUp(): void { - $this->container = new Container(); + $options = new Options(); + $options->checkScope = false; + $this->container = new Container(options: $options); $this->container->bindSingleton(ValidationInterface::class, ValidationProvider::class); } } diff --git a/src/Filters/tests/InputScopeTest.php b/src/Filters/tests/InputScopeTest.php index c7e8797cd..54de734d0 100644 --- a/src/Filters/tests/InputScopeTest.php +++ b/src/Filters/tests/InputScopeTest.php @@ -8,6 +8,7 @@ use Psr\Http\Message\ServerRequestInterface; use Spiral\Filter\InputScope; use Spiral\Filters\InputInterface; +use Spiral\Http\Request\InputManager; final class InputScopeTest extends BaseTestCase { @@ -16,6 +17,7 @@ public function setUp(): void parent::setUp(); $this->container->bindSingleton(InputInterface::class, InputScope::class); + $this->container->bindSingleton(InputManager::class, new InputManager($this->container)); $this->container->bindSingleton( ServerRequestInterface::class, (new ServerRequest('POST', '/test'))->withParsedBody([ diff --git a/src/Framework/Auth/AuthScope.php b/src/Framework/Auth/AuthScope.php index 4c6b4edcd..c9af68b61 100644 --- a/src/Framework/Auth/AuthScope.php +++ b/src/Framework/Auth/AuthScope.php @@ -6,15 +6,17 @@ use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; +use Spiral\Core\Attribute\Proxy; use Spiral\Core\Exception\ScopeException; /** * Provides global access to temporary authentication scope. + * @deprecated Use {@see AuthContextInterface} instead. Will be removed in v4.0. */ final class AuthScope implements AuthContextInterface { public function __construct( - private readonly ContainerInterface $container + #[Proxy] private readonly ContainerInterface $container ) { } diff --git a/src/Framework/Auth/TokenStorageScope.php b/src/Framework/Auth/TokenStorageScope.php index 078fe5c3c..642b146a7 100644 --- a/src/Framework/Auth/TokenStorageScope.php +++ b/src/Framework/Auth/TokenStorageScope.php @@ -7,14 +7,18 @@ use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use Spiral\Auth\Exception\TokenStorageException; +use Spiral\Core\Attribute\Proxy; use Spiral\Core\Attribute\Singleton; use Spiral\Core\Exception\ScopeException; +/** + * @deprecated Use {@see TokenStorageInterface} instead. Will be removed in v4.0. + */ #[Singleton] final class TokenStorageScope implements TokenStorageInterface { public function __construct( - private readonly ContainerInterface $container + #[Proxy] private readonly ContainerInterface $container ) { } diff --git a/src/Framework/Bootloader/Auth/HttpAuthBootloader.php b/src/Framework/Bootloader/Auth/HttpAuthBootloader.php index e30e6fb8b..e6dfd7716 100644 --- a/src/Framework/Bootloader/Auth/HttpAuthBootloader.php +++ b/src/Framework/Bootloader/Auth/HttpAuthBootloader.php @@ -4,8 +4,11 @@ namespace Spiral\Bootloader\Auth; +use Psr\Http\Message\ServerRequestInterface; +use Spiral\Auth\AuthContextInterface; use Spiral\Auth\Config\AuthConfig; use Spiral\Auth\HttpTransportInterface; +use Spiral\Auth\Middleware\AuthMiddleware; use Spiral\Auth\Session\TokenStorage as SessionTokenStorage; use Spiral\Auth\TokenStorageInterface; use Spiral\Auth\TokenStorageProvider; @@ -16,13 +19,19 @@ use Spiral\Boot\AbstractKernel; use Spiral\Boot\Bootloader\Bootloader; use Spiral\Boot\EnvironmentInterface; +use Spiral\Bootloader\Http\Exception\ContextualObjectNotFoundException; +use Spiral\Bootloader\Http\Exception\InvalidRequestScopeException; use Spiral\Bootloader\Http\HttpBootloader; use Spiral\Config\ConfiguratorInterface; use Spiral\Config\Patch\Append; use Spiral\Core\Attribute\Singleton; +use Spiral\Core\BinderInterface; +use Spiral\Core\Config\Proxy; use Spiral\Core\Container\Autowire; use Spiral\Core\FactoryInterface; +use Spiral\Framework\Spiral; use Spiral\Http\Config\HttpConfig; +use Spiral\Http\CurrentRequest; /** * Enables Auth middleware and http transports to read and write tokens in PSR-7 request/response. @@ -30,22 +39,57 @@ #[Singleton] final class HttpAuthBootloader extends Bootloader { - protected const DEPENDENCIES = [ - AuthBootloader::class, - HttpBootloader::class, - ]; - - protected const SINGLETONS = [ - TransportRegistry::class => [self::class, 'transportRegistry'], - TokenStorageInterface::class => [self::class, 'getDefaultTokenStorage'], - TokenStorageProviderInterface::class => TokenStorageProvider::class, - ]; - public function __construct( - private readonly ConfiguratorInterface $config + private readonly ConfiguratorInterface $config, + private readonly BinderInterface $binder, ) { } + public function defineDependencies(): array + { + return [ + AuthBootloader::class, + HttpBootloader::class, + ]; + } + + public function defineBindings(): array + { + $this->binder + ->getBinder(Spiral::Http) + ->bind( + AuthContextInterface::class, + static fn (?ServerRequestInterface $request): AuthContextInterface => + ($request ?? throw new InvalidRequestScopeException(AuthContextInterface::class)) + ->getAttribute(AuthMiddleware::ATTRIBUTE) ?? throw new ContextualObjectNotFoundException( + AuthContextInterface::class, + AuthMiddleware::ATTRIBUTE, + ) + ); + $this->binder->bind(AuthContextInterface::class, new Proxy(AuthContextInterface::class, false)); + + return []; + } + + public function defineSingletons(): array + { + // Default token storage outside of HTTP scope + $this->binder->bindSingleton( + TokenStorageInterface::class, + static fn (TokenStorageProviderInterface $provider): TokenStorageInterface => $provider->getStorage(), + ); + + // Token storage from request attribute in HTTP scope + $this->binder + ->getBinder(Spiral::Http) + ->bindSingleton(TokenStorageInterface::class, [self::class, 'getTokenStorage']); + + return [ + TransportRegistry::class => [self::class, 'transportRegistry'], + TokenStorageProviderInterface::class => TokenStorageProvider::class, + ]; + } + public function init(AbstractKernel $kernel, EnvironmentInterface $env): void { $this->config->setDefaults( @@ -119,8 +163,10 @@ private function transportRegistry(AuthConfig $config, FactoryInterface $factory /** * Get default token storage from provider */ - private function getDefaultTokenStorage(TokenStorageProviderInterface $provider): TokenStorageInterface - { - return $provider->getStorage(); + private function getTokenStorage( + TokenStorageProviderInterface $provider, + ServerRequestInterface $request + ): TokenStorageInterface { + return $request->getAttribute(AuthMiddleware::TOKEN_STORAGE_ATTRIBUTE) ?? $provider->getStorage(); } } diff --git a/src/Framework/Bootloader/Http/CookiesBootloader.php b/src/Framework/Bootloader/Http/CookiesBootloader.php index df83d98e8..0d3e1998d 100644 --- a/src/Framework/Bootloader/Http/CookiesBootloader.php +++ b/src/Framework/Bootloader/Http/CookiesBootloader.php @@ -6,26 +6,33 @@ use Psr\Http\Message\ServerRequestInterface; use Spiral\Boot\Bootloader\Bootloader; +use Spiral\Bootloader\Http\Exception\ContextualObjectNotFoundException; +use Spiral\Bootloader\Http\Exception\InvalidRequestScopeException; use Spiral\Config\ConfiguratorInterface; use Spiral\Config\Patch\Append; use Spiral\Cookies\Config\CookiesConfig; use Spiral\Cookies\CookieQueue; use Spiral\Core\Attribute\Singleton; -use Spiral\Core\Exception\ScopeException; +use Spiral\Core\BinderInterface; +use Spiral\Framework\Spiral; #[Singleton] final class CookiesBootloader extends Bootloader { - protected const BINDINGS = [ - CookieQueue::class => [self::class, 'cookieQueue'], - ]; - public function __construct( - private readonly ConfiguratorInterface $config + private readonly ConfiguratorInterface $config, + private readonly BinderInterface $binder, ) { } - public function init(HttpBootloader $http): void + public function defineBindings(): array + { + $this->binder->getBinder(Spiral::Http)->bind(CookieQueue::class, [self::class, 'cookieQueue']); + + return []; + } + + public function init(): void { $this->config->setDefaults( CookiesConfig::CONFIG, @@ -45,16 +52,15 @@ public function whitelistCookie(string $cookie): void $this->config->modify(CookiesConfig::CONFIG, new Append('excluded', null, $cookie)); } - /** - * @noRector RemoveUnusedPrivateMethodRector - */ - private function cookieQueue(ServerRequestInterface $request): CookieQueue + private function cookieQueue(?ServerRequestInterface $request): CookieQueue { - $cookieQueue = $request->getAttribute(CookieQueue::ATTRIBUTE, null); - if ($cookieQueue === null) { - throw new ScopeException('Unable to resolve CookieQueue, invalid request scope'); + if ($request === null) { + throw new InvalidRequestScopeException(CookieQueue::class); } - return $cookieQueue; + return $request->getAttribute(CookieQueue::ATTRIBUTE) ?? throw new ContextualObjectNotFoundException( + CookieQueue::class, + CookieQueue::ATTRIBUTE, + ); } } diff --git a/src/Framework/Bootloader/Http/Exception/ContextualObjectNotFoundException.php b/src/Framework/Bootloader/Http/Exception/ContextualObjectNotFoundException.php new file mode 100644 index 000000000..c2ee5c219 --- /dev/null +++ b/src/Framework/Bootloader/Http/Exception/ContextualObjectNotFoundException.php @@ -0,0 +1,31 @@ +value); + } +} diff --git a/src/Framework/Bootloader/Http/HttpBootloader.php b/src/Framework/Bootloader/Http/HttpBootloader.php index 5d33ca54e..4ce96cae1 100644 --- a/src/Framework/Bootloader/Http/HttpBootloader.php +++ b/src/Framework/Bootloader/Http/HttpBootloader.php @@ -6,6 +6,7 @@ use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Spiral\Boot\Bootloader\Bootloader; @@ -18,6 +19,8 @@ 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; @@ -44,7 +47,16 @@ public function defineDependencies(): array public function defineSingletons(): array { - $this->binder->getBinder(Spiral::Http)->bindSingleton(Http::class, [self::class, 'httpCore']); + $httpBinder = $this->binder->getBinder(Spiral::Http); + + $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.', + ) + ); /** * @deprecated since v3.12. Will be removed in v4.0. diff --git a/src/Framework/Bootloader/Http/PaginationBootloader.php b/src/Framework/Bootloader/Http/PaginationBootloader.php index 0d4a0c28b..c8355b636 100644 --- a/src/Framework/Bootloader/Http/PaginationBootloader.php +++ b/src/Framework/Bootloader/Http/PaginationBootloader.php @@ -27,9 +27,9 @@ public function defineDependencies(): array public function defineSingletons(): array { - $this->binder - ->getBinder(Spiral::HttpRequest) - ->bindSingleton(PaginationProviderInterface::class, PaginationFactory::class); + $httpRequest = $this->binder->getBinder(Spiral::HttpRequest); + $httpRequest->bindSingleton(PaginationFactory::class, PaginationFactory::class); + $httpRequest->bindSingleton(PaginationProviderInterface::class, PaginationFactory::class); $this->binder->bind( PaginationProviderInterface::class, diff --git a/src/Framework/Bootloader/Http/RouterBootloader.php b/src/Framework/Bootloader/Http/RouterBootloader.php index a6c86baa1..6d2f315fc 100644 --- a/src/Framework/Bootloader/Http/RouterBootloader.php +++ b/src/Framework/Bootloader/Http/RouterBootloader.php @@ -12,10 +12,12 @@ use Spiral\Boot\Bootloader\Bootloader; use Spiral\Config\ConfiguratorInterface; use Spiral\Core\Attribute\Proxy; +use Spiral\Core\BinderInterface; use Spiral\Core\Core; use Spiral\Core\CoreInterface; use Spiral\Core\Exception\ScopeException; use Spiral\Framework\Kernel; +use Spiral\Framework\Spiral; use Spiral\Http\Config\HttpConfig; use Spiral\Router\GroupRegistry; use Spiral\Router\Loader\Configurator\RoutingConfigurator; @@ -35,28 +37,38 @@ final class RouterBootloader extends Bootloader { - protected const DEPENDENCIES = [ - HttpBootloader::class, - TelemetryBootloader::class, - ]; - - protected const SINGLETONS = [ - CoreInterface::class => Core::class, - RouterInterface::class => [self::class, 'router'], - RouteInterface::class => [self::class, 'route'], - RequestHandlerInterface::class => RouterInterface::class, - LoaderInterface::class => DelegatingLoader::class, - LoaderRegistryInterface::class => [self::class, 'initRegistry'], - GroupRegistry::class => GroupRegistry::class, - RoutingConfigurator::class => RoutingConfigurator::class, - RoutePatternRegistryInterface::class => DefaultPatternRegistry::class, - ]; - public function __construct( - private readonly ConfiguratorInterface $config + private readonly ConfiguratorInterface $config, + private readonly BinderInterface $binder, ) { } + public function defineDependencies(): array + { + return [ + HttpBootloader::class, + TelemetryBootloader::class, + ]; + } + + public function defineSingletons(): array + { + $this->binder + ->getBinder(Spiral::HttpRequest) + ->bindSingleton(RouteInterface::class, [self::class, 'route']); + + return [ + CoreInterface::class => Core::class, + RouterInterface::class => [self::class, 'router'], + RequestHandlerInterface::class => RouterInterface::class, + LoaderInterface::class => DelegatingLoader::class, + LoaderRegistryInterface::class => [self::class, 'initRegistry'], + GroupRegistry::class => GroupRegistry::class, + RoutingConfigurator::class => RoutingConfigurator::class, + RoutePatternRegistryInterface::class => DefaultPatternRegistry::class, + ]; + } + public function boot(AbstractKernel $kernel): void { $configuratorCallback = static function (RouterInterface $router, RoutingConfigurator $routes): void { diff --git a/src/Framework/Bootloader/Http/SessionBootloader.php b/src/Framework/Bootloader/Http/SessionBootloader.php index 11d6e8b3f..4939d9ffe 100644 --- a/src/Framework/Bootloader/Http/SessionBootloader.php +++ b/src/Framework/Bootloader/Http/SessionBootloader.php @@ -4,20 +4,58 @@ namespace Spiral\Bootloader\Http; +use Psr\Http\Message\ServerRequestInterface; use Spiral\Boot\Bootloader\Bootloader; use Spiral\Boot\DirectoriesInterface; +use Spiral\Bootloader\Http\Exception\ContextualObjectNotFoundException; +use Spiral\Bootloader\Http\Exception\InvalidRequestScopeException; use Spiral\Config\ConfiguratorInterface; +use Spiral\Core\BinderInterface; +use Spiral\Core\Config\Proxy; use Spiral\Core\Container\Autowire; +use Spiral\Framework\Spiral; use Spiral\Session\Config\SessionConfig; use Spiral\Session\Handler\FileHandler; +use Spiral\Session\Middleware\SessionMiddleware; use Spiral\Session\SessionFactory; use Spiral\Session\SessionFactoryInterface; +use Spiral\Session\SessionInterface; final class SessionBootloader extends Bootloader { - protected const SINGLETONS = [ - SessionFactoryInterface::class => SessionFactory::class, - ]; + public function __construct( + private readonly BinderInterface $binder, + ) { + } + + public function defineBindings(): array + { + $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, + ) + ); + $this->binder->bind(SessionInterface::class, new Proxy(SessionInterface::class, false)); + + return []; + } + + public function defineSingletons(): array + { + $http = $this->binder->getBinder(Spiral::Http); + $http->bindSingleton(SessionFactory::class, SessionFactory::class); + $http->bindSingleton(SessionFactoryInterface::class, SessionFactory::class); + + $this->binder->bind(SessionFactoryInterface::class, new Proxy(SessionFactoryInterface::class, true)); + + return []; + } /** * Automatically registers session starter middleware and excludes session cookie from diff --git a/src/Framework/Bootloader/Security/FiltersBootloader.php b/src/Framework/Bootloader/Security/FiltersBootloader.php index 915b2556c..010b2d10e 100644 --- a/src/Framework/Bootloader/Security/FiltersBootloader.php +++ b/src/Framework/Bootloader/Security/FiltersBootloader.php @@ -11,6 +11,7 @@ use Spiral\Config\Patch\Append; use Spiral\Core\Attribute\Singleton; use Spiral\Core\BinderInterface; +use Spiral\Core\Config\Proxy; use Spiral\Core\Container; use Spiral\Core\CoreInterceptorInterface; use Spiral\Core\InterceptableCore; @@ -28,6 +29,9 @@ use Spiral\Filters\Model\Mapper\CasterRegistry; use Spiral\Filters\Model\Mapper\CasterRegistryInterface; use Spiral\Filters\Model\Mapper\UuidCaster; +use Spiral\Framework\Spiral; +use Spiral\Http\Config\HttpConfig; +use Spiral\Http\Request\InputManager; /** * @implements Container\InjectorInterface @@ -35,12 +39,6 @@ #[Singleton] final class FiltersBootloader extends Bootloader implements Container\InjectorInterface { - protected const SINGLETONS = [ - FilterProviderInterface::class => [self::class, 'initFilterProvider'], - InputInterface::class => InputScope::class, - CasterRegistryInterface::class => [self::class, 'initCasterRegistry'], - ]; - public function __construct( private readonly ContainerInterface $container, private readonly BinderInterface $binder, @@ -48,6 +46,25 @@ public function __construct( ) { } + public function defineSingletons(): array + { + $this->binder + ->getBinder(Spiral::HttpRequest) + ->bindSingleton( + InputInterface::class, + static function (ContainerInterface $container, HttpConfig $config): InputScope { + return new InputScope(new InputManager($container, $config)); + } + ); + + $this->binder->bind(InputInterface::class, new Proxy(InputInterface::class, true)); + + return [ + FilterProviderInterface::class => [self::class, 'initFilterProvider'], + CasterRegistryInterface::class => [self::class, 'initCasterRegistry'], + ]; + } + /** * Declare Filter injection. */ diff --git a/src/Framework/Cookies/CookieManager.php b/src/Framework/Cookies/CookieManager.php index 01bedb234..4b750e6ff 100644 --- a/src/Framework/Cookies/CookieManager.php +++ b/src/Framework/Cookies/CookieManager.php @@ -7,17 +7,21 @@ use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use Psr\Http\Message\ServerRequestInterface; +use Spiral\Core\Attribute\Proxy; +use Spiral\Core\Attribute\Scope; use Spiral\Core\Attribute\Singleton; use Spiral\Core\Exception\ScopeException; +use Spiral\Framework\Spiral; /** * Cookies manages provides the ability to write and read cookies from the active request/response scope. */ #[Singleton] +#[Scope(Spiral::HttpRequest)] final class CookieManager { public function __construct( - private readonly ContainerInterface $container + #[Proxy] private readonly ContainerInterface $container, ) { } diff --git a/src/Framework/Filter/InputScope.php b/src/Framework/Filter/InputScope.php index 8e04df2d9..b6fd80ece 100644 --- a/src/Framework/Filter/InputScope.php +++ b/src/Framework/Filter/InputScope.php @@ -4,13 +4,16 @@ namespace Spiral\Filter; +use Spiral\Core\Attribute\Scope; use Spiral\Filters\Exception\InputException; use Spiral\Filters\InputInterface; +use Spiral\Framework\Spiral; use Spiral\Http\Request\InputManager; /** * Provides ability to use http request scope as filters input. */ +#[Scope(Spiral::HttpRequest)] final class InputScope implements InputInterface { public function __construct( diff --git a/src/Framework/Session/Middleware/SessionMiddleware.php b/src/Framework/Session/Middleware/SessionMiddleware.php index ee134e066..47f850b43 100644 --- a/src/Framework/Session/Middleware/SessionMiddleware.php +++ b/src/Framework/Session/Middleware/SessionMiddleware.php @@ -25,6 +25,9 @@ final class SessionMiddleware implements MiddlewareInterface // Header set used to sign session private const SIGNATURE_HEADERS = ['User-Agent', 'Accept-Language', 'Accept-Encoding']; + /** + * @param ScopeInterface $scope Deprecated, will be removed in v4.0. + */ public function __construct( private readonly SessionConfig $config, private readonly HttpConfig $httpConfig, @@ -46,10 +49,7 @@ public function process(Request $request, Handler $handler): Response ); try { - $response = $this->scope->runScope( - [SessionInterface::class => $session], - static fn () => $handler->handle($request->withAttribute(self::ATTRIBUTE, $session)) - ); + $response = $handler->handle($request->withAttribute(self::ATTRIBUTE, $session)); } catch (\Throwable $e) { $session->abort(); throw $e; diff --git a/src/Framework/Session/SectionScope.php b/src/Framework/Session/SectionScope.php index 3990b9ddb..278c3c131 100644 --- a/src/Framework/Session/SectionScope.php +++ b/src/Framework/Session/SectionScope.php @@ -4,6 +4,9 @@ namespace Spiral\Session; +/** + * @deprecated Use {@see SessionInterface::getSection()} instead. Will be removed in v4.0. + */ final class SectionScope implements SessionSectionInterface { public function __construct( diff --git a/src/Framework/Session/SessionScope.php b/src/Framework/Session/SessionScope.php index 3a974ae46..9ac2508be 100644 --- a/src/Framework/Session/SessionScope.php +++ b/src/Framework/Session/SessionScope.php @@ -6,11 +6,13 @@ use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; +use Spiral\Core\Attribute\Proxy; use Spiral\Core\Attribute\Singleton; use Spiral\Core\Exception\ScopeException; /** * Provides access to the currently active session scope. + * @deprecated Use {@see SessionInterface} instead. Will be removed in v4.0. */ #[Singleton] final class SessionScope implements SessionInterface @@ -19,7 +21,7 @@ final class SessionScope implements SessionInterface private const DEFAULT_SECTION = '_DEFAULT'; public function __construct( - private readonly ContainerInterface $container + #[Proxy] private readonly ContainerInterface $container ) { } diff --git a/src/Http/src/CurrentRequest.php b/src/Http/src/CurrentRequest.php new file mode 100644 index 000000000..4feee69ca --- /dev/null +++ b/src/Http/src/CurrentRequest.php @@ -0,0 +1,29 @@ +request = $request; + } + + public function get(): ?ServerRequestInterface + { + return $this->request; + } +} diff --git a/src/Http/src/Http.php b/src/Http/src/Http.php index 9fe1bafe4..8ce37f96b 100644 --- a/src/Http/src/Http.php +++ b/src/Http/src/Http.php @@ -31,7 +31,8 @@ public function __construct( private readonly Pipeline $pipeline, private readonly ResponseFactoryInterface $responseFactory, private readonly ContainerInterface $container, - ?TracerFactoryInterface $tracerFactory = null + ?TracerFactoryInterface $tracerFactory = null, + private readonly ?EventDispatcherInterface $dispatcher = null, ) { foreach ($this->config->getMiddleware() as $middleware) { $this->pipeline->pushMiddleware($this->container->get($middleware)); @@ -60,12 +61,10 @@ public function setHandler(callable|RequestHandlerInterface $handler): self */ public function handle(ServerRequestInterface $request): ResponseInterface { - $callback = function (SpanInterface $span) use ($request): ResponseInterface { - $dispatcher = $this->container->has(EventDispatcherInterface::class) - ? $this->container->get(EventDispatcherInterface::class) - : null; + $callback = function (SpanInterface $span, CurrentRequest $currentRequest) use ($request): ResponseInterface { + $currentRequest->set($request); - $dispatcher?->dispatch(new RequestReceived($request)); + $this->dispatcher?->dispatch(new RequestReceived($request)); if ($this->handler === null) { throw new HttpException('Unable to run HttpCore, no handler is set.'); @@ -84,7 +83,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface ) ->setStatus($response->getStatusCode() < 500 ? 'OK' : 'ERROR'); - $dispatcher?->dispatch(new RequestHandled($request, $response)); + $this->dispatcher?->dispatch(new RequestHandled($request, $response)); return $response; }; diff --git a/src/Http/src/Pipeline.php b/src/Http/src/Pipeline.php index 773afb0ea..79dc91ae1 100644 --- a/src/Http/src/Pipeline.php +++ b/src/Http/src/Pipeline.php @@ -10,6 +10,7 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Spiral\Core\Attribute\Proxy; +use Spiral\Core\ContainerScope; use Spiral\Core\ScopeInterface; use Spiral\Http\Event\MiddlewareProcessing; use Spiral\Http\Exception\PipelineException; @@ -62,42 +63,50 @@ public function handle(Request $request): Response throw new PipelineException('Unable to run pipeline, no handler given.'); } - $position = $this->position++; - if (isset($this->middleware[$position])) { + // todo: find a better solution in the Spiral v4.0 + /** @var CurrentRequest|null $currentRequest */ + $currentRequest = ContainerScope::getContainer()?->get(CurrentRequest::class); + + $previousRequest = $currentRequest?->get(); + $currentRequest?->set($request); + try { + $position = $this->position++; + if (!isset($this->middleware[$position])) { + return $this->handler->handle($request); + } + $middleware = $this->middleware[$position]; $this->dispatcher?->dispatch(new MiddlewareProcessing($request, $middleware)); + $callback = function (SpanInterface $span) use ($request, $middleware): Response { + $response = $middleware->process($request, $this); + + $span + ->setAttribute( + 'http.status_code', + $response->getStatusCode() + ) + ->setAttribute( + 'http.response_content_length', + $response->getHeaderLine('Content-Length') ?: $response->getBody()->getSize() + ) + ->setStatus($response->getStatusCode() < 500 ? 'OK' : 'ERROR'); + + return $response; + }; + return $this->tracer->trace( name: \sprintf('Middleware processing [%s]', $middleware::class), - callback: function (SpanInterface $span) use ($request, $middleware): Response { - $response = $middleware->process($request, $this); - - $span - ->setAttribute( - 'http.status_code', - $response->getStatusCode() - ) - ->setAttribute( - 'http.response_content_length', - $response->getHeaderLine('Content-Length') ?: $response->getBody()->getSize() - ) - ->setStatus($response->getStatusCode() < 500 ? 'OK' : 'ERROR'); - - return $response; - }, - scoped: true, + callback: $callback, attributes: [ 'http.middleware' => $middleware::class, - ] + ], + scoped: true ); + } finally { + if ($previousRequest !== null) { + $currentRequest?->set($previousRequest); + } } - - $handler = $this->handler; - - // TODO: Can we remove this scope? - return $this->scope->runScope( - [Request::class => $request], - static fn (): Response => $handler->handle($request) - ); } } diff --git a/src/Http/src/Request/InputManager.php b/src/Http/src/Request/InputManager.php index 6e3629934..66f2dd3fc 100644 --- a/src/Http/src/Request/InputManager.php +++ b/src/Http/src/Request/InputManager.php @@ -9,6 +9,8 @@ use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\UriInterface; +use Spiral\Core\Attribute\Proxy; +use Spiral\Core\Attribute\Scope; use Spiral\Core\Attribute\Singleton; use Spiral\Core\Exception\ScopeException; use Spiral\Http\Config\HttpConfig; @@ -48,6 +50,7 @@ * @method mixed attribute(string $name, mixed $default = null) */ #[Singleton] +#[Scope('http.request')] final class InputManager { /** @@ -115,7 +118,7 @@ final class InputManager public function __construct( /** @invisible */ - private readonly ContainerInterface $container, + #[Proxy] private readonly ContainerInterface $container, /** @invisible */ HttpConfig $config = new HttpConfig() ) { diff --git a/src/Http/tests/HttpTest.php b/src/Http/tests/HttpTest.php index 6fd322a11..755d74c97 100644 --- a/src/Http/tests/HttpTest.php +++ b/src/Http/tests/HttpTest.php @@ -7,9 +7,8 @@ use Mockery as m; use PHPUnit\Framework\TestCase; use Psr\EventDispatcher\EventDispatcherInterface; -use Psr\Http\Message\ServerRequestInterface; use Spiral\Core\Container; -use Spiral\Core\ContainerScope; +use Spiral\Core\Options; use Spiral\Http\CallableHandler; use Spiral\Http\Config\HttpConfig; use Spiral\Http\Event\RequestHandled; @@ -23,7 +22,7 @@ use Spiral\Tests\Http\Diactoros\ResponseFactory; use Nyholm\Psr7\ServerRequest; -class HttpTest extends TestCase +final class HttpTest extends TestCase { use m\Adapter\Phpunit\MockeryPHPUnitIntegration; @@ -31,7 +30,9 @@ class HttpTest extends TestCase public function setUp(): void { - $this->container = new Container(); + $options = new Options(); + $options->checkScope = false; + $this->container = new Container(options: $options); $this->container->bind(TracerInterface::class, new NullTracer($this->container)); } @@ -223,20 +224,6 @@ public function testMiddlewareTraitReversed(): void $this->assertSame('hello?', (string)$response->getBody()); } - public function testScope(): void - { - $core = $this->getCore(); - - $core->setHandler(function () { - $this->assertTrue(ContainerScope::getContainer()->has(ServerRequestInterface::class)); - - return 'OK'; - }); - - $response = $core->handle(new ServerRequest('GET', '')); - $this->assertSame('OK', (string)$response->getBody()); - } - public function testPassException(): void { $this->expectException(\RuntimeException::class); @@ -293,7 +280,7 @@ public function testPassingTracerIntoScope(): void $tracerFactory->shouldReceive('make') ->once() ->with(['foo' => ['bar']]) - ->andReturn($tracer = new NullTracer()); + ->andReturn(new NullTracer($this->container)); $response = $http->handle($request); $this->assertSame('hello world', (string)$response->getBody()); @@ -307,7 +294,10 @@ protected function getCore(array $middleware = []): Http $config, new Pipeline($this->container), new ResponseFactory($config), - $this->container + $this->container, + dispatcher: $this->container->has(EventDispatcherInterface::class) + ? $this->container->get(EventDispatcherInterface::class) + : null, ); } diff --git a/src/Http/tests/PipelineTest.php b/src/Http/tests/PipelineTest.php index 1dc350ae4..492488954 100644 --- a/src/Http/tests/PipelineTest.php +++ b/src/Http/tests/PipelineTest.php @@ -4,28 +4,29 @@ namespace Spiral\Tests\Http; -use PHPUnit\Framework\TestCase; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Spiral\Core\Container; +use Spiral\Core\ContainerScope; +use Spiral\Core\ScopeInterface; use Spiral\Http\CallableHandler; use Spiral\Http\Config\HttpConfig; +use Spiral\Http\CurrentRequest; use Spiral\Http\Event\MiddlewareProcessing; use Spiral\Http\Exception\PipelineException; use Spiral\Http\Pipeline; use Spiral\Telemetry\NullTracer; -use Spiral\Telemetry\NullTracerFactory; +use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Http\Diactoros\ResponseFactory; use Nyholm\Psr7\ServerRequest; -class PipelineTest extends TestCase +final class PipelineTest extends TestCase { public function testTarget(): void { - $pipeline = new Pipeline(new Container()); + $pipeline = new Pipeline($this->container); $handler = new CallableHandler(function () { return 'response'; @@ -40,7 +41,7 @@ public function testTarget(): void public function testHandle(): void { - $pipeline = new Pipeline(new Container()); + $pipeline = new Pipeline($this->container); $handler = new CallableHandler(function () { return 'response'; @@ -57,7 +58,7 @@ public function testHandleException(): void { $this->expectException(PipelineException::class); - $pipeline = new Pipeline(new Container()); + $pipeline = new Pipeline($this->container); $pipeline->handle(new ServerRequest('GET', '')); } @@ -80,16 +81,51 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface ->method('dispatch') ->with(new MiddlewareProcessing($request, $middleware)); - $container = new Container(); - - $pipeline = new Pipeline( - $container, - $dispatcher, - new NullTracer($container) - ); + $pipeline = new Pipeline($this->container, $dispatcher, new NullTracer($this->container)); $pipeline->pushMiddleware($middleware); $pipeline->withHandler($handler)->handle($request); } + + public function testRequestResetThroughPipeline(): void + { + $this->container->getBinder('http') + ->bindSingleton(CurrentRequest::class, new CurrentRequest()); + $this->container->getBinder('http') + ->bind(ServerRequestInterface::class, static fn(CurrentRequest $cr) => $cr->get()); + + $middleware = new class implements MiddlewareInterface { + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler, + ): ResponseInterface { + $cRequest = ContainerScope::getContainer()->get(ServerRequestInterface::class); + PipelineTest::assertSame($cRequest, $request); + + $response = $handler->handle($request->withAttribute('foo', 'bar')); + + $cRequest = ContainerScope::getContainer()->get(ServerRequestInterface::class); + PipelineTest::assertSame($cRequest, $request); + return $response; + } + }; + + $this->container->runScope( + new \Spiral\Core\Scope(name: 'http'), + function (ScopeInterface $c) use ($middleware) { + $request = new ServerRequest('GET', ''); + $handler = new CallableHandler(function () { + return 'response'; + }, new ResponseFactory(new HttpConfig(['headers' => []]))); + + $pipeline = new Pipeline($c, null, new NullTracer($c)); + + $pipeline->pushMiddleware($middleware); + $pipeline->pushMiddleware($middleware); + + $pipeline->withHandler($handler)->handle($request); + } + ); + } } diff --git a/src/Http/tests/TestCase.php b/src/Http/tests/TestCase.php new file mode 100644 index 000000000..6c8b983fa --- /dev/null +++ b/src/Http/tests/TestCase.php @@ -0,0 +1,23 @@ +checkScope = false; + $this->container = new Container(options: $options); + $this->container->bind(TracerInterface::class, new NullTracer($this->container)); + } +} diff --git a/src/Router/tests/BaseTestCase.php b/src/Router/tests/BaseTestCase.php index 2c8ced74e..27af8ec1d 100644 --- a/src/Router/tests/BaseTestCase.php +++ b/src/Router/tests/BaseTestCase.php @@ -12,7 +12,9 @@ use Spiral\Core\Container; use Spiral\Core\Container\Autowire; use Spiral\Core\CoreInterface; +use Spiral\Core\Options; use Spiral\Http\Config\HttpConfig; +use Spiral\Http\CurrentRequest; use Spiral\Router\GroupRegistry; use Spiral\Router\Loader\Configurator\RoutingConfigurator; use Spiral\Router\Loader\DelegatingLoader; @@ -73,7 +75,9 @@ public static function middlewaresDataProvider(): \Traversable private function initContainer(): void { - $this->container = new Container(); + $options = new Options(); + $options->checkScope = false; + $this->container = new Container(options: $options); $this->container->bind(TracerInterface::class, new NullTracer($this->container)); $this->container->bind(ResponseFactoryInterface::class, new ResponseFactory(new HttpConfig(['headers' => []]))); $this->container->bind(UriFactoryInterface::class, new UriFactory()); @@ -90,6 +94,9 @@ private function initContainer(): void $this->container->bind(CoreInterface::class, Core::class); $this->container->bindSingleton(GroupRegistry::class, GroupRegistry::class); $this->container->bindSingleton(RoutingConfigurator::class, RoutingConfigurator::class); + $this->container + ->getBinder('http') + ->bindSingleton(CurrentRequest::class, CurrentRequest::class); } private function initRouter(): void diff --git a/src/Router/tests/PipelineFactoryTest.php b/src/Router/tests/PipelineFactoryTest.php index a4125ba02..f090717e3 100644 --- a/src/Router/tests/PipelineFactoryTest.php +++ b/src/Router/tests/PipelineFactoryTest.php @@ -15,6 +15,7 @@ use Spiral\Core\Container\Autowire; use Spiral\Core\FactoryInterface; use Spiral\Core\ScopeInterface; +use Spiral\Http\CurrentRequest; use Spiral\Http\Pipeline; use Spiral\Router\Exception\RouteException; use Spiral\Router\PipelineFactory; @@ -22,17 +23,15 @@ final class PipelineFactoryTest extends \PHPUnit\Framework\TestCase { - use m\Adapter\Phpunit\MockeryPHPUnitIntegration; - - private ContainerInterface&m\MockInterface $container; - private FactoryInterface&m\MockInterface $factory; + private ContainerInterface $container; + private FactoryInterface $factory; private PipelineFactory $pipeline; protected function setUp(): void { parent::setUp(); - $this->container = m::mock(ContainerInterface::class); + $this->container = $this->createMock(ContainerInterface::class); $this->factory = m::mock(FactoryInterface::class); $this->pipeline = new PipelineFactory($this->container, $this->factory); @@ -41,7 +40,7 @@ protected function setUp(): void public function testCreatesFromArrayWithPipeline(): void { $newPipeline = new Pipeline( - scope: m::mock(ScopeInterface::class) + scope: $this->createMock(ScopeInterface::class), ); $this->assertSame( @@ -53,13 +52,14 @@ public function testCreatesFromArrayWithPipeline(): void public function testCreates(): void { $container = new Container(); + $container->bindSingleton(CurrentRequest::class, new CurrentRequest()); $this->factory ->shouldReceive('make') ->once() ->with(Pipeline::class) ->andReturn($p = new Pipeline( - $scope = m::mock(ScopeInterface::class), + $this->createMock(ScopeInterface::class), tracer: new NullTracer($container) )); @@ -67,17 +67,18 @@ public function testCreates(): void ->shouldReceive('make') ->once() ->with('bar', []) - ->andReturn($middleware5 = m::mock(MiddlewareInterface::class)); + ->andReturn($middleware5 = $this->createMock(MiddlewareInterface::class)); - $this->container->shouldReceive('get') - ->once() + $this->container + ->expects($this->once()) + ->method('get') ->with('foo') - ->andReturn($middleware4 = m::mock(MiddlewareInterface::class)); + ->willReturn($middleware4 = $this->createMock(MiddlewareInterface::class)); $this->assertSame($p, $this->pipeline->createWithMiddleware([ 'foo', - $middleware1 = m::mock(MiddlewareInterface::class), - $middleware2 = m::mock(MiddlewareInterface::class), + $middleware1 = $this->createMock(MiddlewareInterface::class), + $middleware2 = $this->createMock(MiddlewareInterface::class), new Autowire('bar'), ])); @@ -85,22 +86,25 @@ public function testCreates(): void return $handler->handle($request); }; - $middleware1->shouldReceive('process')->once()->andReturnUsing($handle); - $middleware2->shouldReceive('process')->once()->andReturnUsing($handle); - $middleware4->shouldReceive('process')->once()->andReturnUsing($handle); - $middleware5->shouldReceive('process')->once()->andReturnUsing($handle); - - $scope->shouldReceive('runScope') - ->once() - ->andReturn($response = m::mock(ResponseInterface::class)); + $middleware1->expects($this->once())->method('process')->willReturnCallback($handle); + $middleware2->expects($this->once())->method('process')->willReturnCallback($handle); + $middleware4->expects($this->once())->method('process')->willReturnCallback($handle); + $middleware5->expects($this->once())->method('process')->willReturnCallback($handle); + $response = $this->createMock(ResponseInterface::class); $response - ->shouldReceive('getHeaderLine')->with('Content-Length')->andReturn('test') - ->shouldReceive('getStatusCode')->andReturn(200); + ->expects($this->exactly(4))->method('getHeaderLine')->with('Content-Length')->willReturn('test'); + $response->expects($this->exactly(8))->method('getStatusCode')->willReturn(200); + + $requestHandler = $this->createMock(RequestHandlerInterface::class); + $requestHandler + ->expects($this->once()) + ->method('handle') + ->willReturn($response); $p - ->withHandler(m::mock(RequestHandlerInterface::class)) - ->handle(m::mock(ServerRequestInterface::class)); + ->withHandler($requestHandler) + ->handle($this->createMock(ServerRequestInterface::class)); } #[DataProvider('invalidTypeDataProvider')] diff --git a/src/Session/src/Session.php b/src/Session/src/Session.php index 23379ac52..2b4ccac9a 100644 --- a/src/Session/src/Session.php +++ b/src/Session/src/Session.php @@ -4,6 +4,7 @@ namespace Spiral\Session; +use Spiral\Core\Attribute\Scope; use Spiral\Session\Exception\SessionException; /** @@ -14,6 +15,7 @@ * * @see https://www.owasp.org/index.php/Session_Management_Cheat_Sheet */ +#[Scope('http')] final class Session implements SessionInterface { /** diff --git a/src/Session/src/SessionFactory.php b/src/Session/src/SessionFactory.php index e8b36ef54..f1873f07b 100644 --- a/src/Session/src/SessionFactory.php +++ b/src/Session/src/SessionFactory.php @@ -5,6 +5,7 @@ namespace Spiral\Session; use Psr\Container\ContainerExceptionInterface; +use Spiral\Core\Attribute\Scope; use Spiral\Core\Attribute\Singleton; use Spiral\Core\FactoryInterface; use Spiral\Session\Config\SessionConfig; @@ -15,6 +16,7 @@ * Initiates session instance and configures session handlers. */ #[Singleton] +#[Scope('http')] final class SessionFactory implements SessionFactoryInterface { public function __construct( diff --git a/src/Session/tests/FactoryTest.php b/src/Session/tests/FactoryTest.php index 0129d582e..dad2fdba3 100644 --- a/src/Session/tests/FactoryTest.php +++ b/src/Session/tests/FactoryTest.php @@ -4,8 +4,6 @@ namespace Spiral\Tests\Session; -use PHPUnit\Framework\TestCase; -use Spiral\Core\Container; use Spiral\Session\Config\SessionConfig; use Spiral\Session\Exception\SessionException; use Spiral\Session\Handler\FileHandler; @@ -13,7 +11,7 @@ use Spiral\Session\SessionFactory; use Spiral\Session\SessionInterface; -class FactoryTest extends TestCase +final class FactoryTest extends TestCase { public function tearDown(): void { @@ -33,7 +31,7 @@ public function testConstructInvalid(): void 'handlers' => [ //No directory ] - ]), new Container()); + ]), $this->container); $factory->initSession('sig', 'sessionid'); } @@ -49,7 +47,7 @@ public function testAlreadyStarted(): void 'handlers' => [ //No directory ] - ]), new Container()); + ]), $this->container); $factory->initSession('sig', 'sessionid'); } @@ -64,9 +62,9 @@ public function testMultipleSessions(): void 'secure' => false, 'handler' => null, 'handlers' => [] - ]), $c = new Container()); + ]), $this->container); - $c->bind(SessionInterface::class, Session::class); + $this->container->bind(SessionInterface::class, Session::class); $session = $factory->initSession('sig'); $session->resume(); diff --git a/src/Session/tests/SessionTest.php b/src/Session/tests/SessionTest.php index a043259fa..0c12ce335 100644 --- a/src/Session/tests/SessionTest.php +++ b/src/Session/tests/SessionTest.php @@ -4,7 +4,6 @@ namespace Spiral\Tests\Session; -use PHPUnit\Framework\TestCase; use Spiral\Core\Container; use Spiral\Files\Files; use Spiral\Files\FilesInterface; @@ -16,21 +15,17 @@ use Spiral\Session\SessionInterface; use Spiral\Session\SessionSection; -class SessionTest extends TestCase +final class SessionTest extends TestCase { - - /** - * @var SessionFactory - */ - private $factory; + private SessionFactory $factory; public function setUp(): void { - $container = new Container(); - $container->bind(FilesInterface::class, Files::class); + parent::setUp(); - $container->bind(SessionInterface::class, Session::class); - $container->bind(SessionSectionInterface::class, SessionSection::class); + $this->container->bind(FilesInterface::class, Files::class); + $this->container->bind(SessionInterface::class, Session::class); + $this->container->bind(SessionSectionInterface::class, SessionSection::class); $this->factory = new SessionFactory(new SessionConfig([ 'lifetime' => 86400, @@ -39,7 +34,7 @@ public function setUp(): void 'handler' => new Container\Autowire(FileHandler::class, [ 'directory' => sys_get_temp_dir() ]), - ]), $container); + ]), $this->container); } public function tearDown(): void diff --git a/src/Session/tests/TestCase.php b/src/Session/tests/TestCase.php new file mode 100644 index 000000000..354860354 --- /dev/null +++ b/src/Session/tests/TestCase.php @@ -0,0 +1,20 @@ +checkScope = false; + $this->container = new Container(options: $options); + } +} diff --git a/tests/Framework/AuthHttp/Middleware/AuthMiddlewareTest.php b/tests/Framework/AuthHttp/Middleware/AuthMiddlewareTest.php index 7b9e87647..3ec7144ed 100644 --- a/tests/Framework/AuthHttp/Middleware/AuthMiddlewareTest.php +++ b/tests/Framework/AuthHttp/Middleware/AuthMiddlewareTest.php @@ -38,14 +38,6 @@ public function testTokenStorageInterfaceShouldBeBound(): void $this->assertSame($storage, $ref->invoke($scope)); }); - $scope = $this->getContainer()->get(TokenStorageScope::class); - $ref = new \ReflectionMethod($scope, 'getTokenStorage'); - $this->assertNotInstanceOf($storage::class, $ref->invoke($scope)); - $this->fakeHttp()->get('/'); - - $scope = $this->getContainer()->get(TokenStorageScope::class); - $ref = new \ReflectionMethod($scope, 'getTokenStorage'); - $this->assertNotInstanceOf($storage::class, $ref->invoke($scope)); } } diff --git a/tests/Framework/Bootloader/Http/CookiesBootloaderTest.php b/tests/Framework/Bootloader/Http/CookiesBootloaderTest.php index 0754a2142..4e6544cc4 100644 --- a/tests/Framework/Bootloader/Http/CookiesBootloaderTest.php +++ b/tests/Framework/Bootloader/Http/CookiesBootloaderTest.php @@ -6,35 +6,50 @@ use Psr\Http\Message\ServerRequestInterface; use Spiral\Bootloader\Http\CookiesBootloader; +use Spiral\Bootloader\Http\Exception\ContextualObjectNotFoundException; +use Spiral\Bootloader\Http\Exception\InvalidRequestScopeException; use Spiral\Config\ConfigManager; use Spiral\Config\LoaderInterface; use Spiral\Cookies\Config\CookiesConfig; use Spiral\Cookies\CookieQueue; +use Spiral\Framework\Spiral; +use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Framework\BaseTestCase; final class CookiesBootloaderTest extends BaseTestCase { + #[TestScope(Spiral::Http)] public function testCookieQueueBinding(): void { $request = $this->mockContainer(ServerRequestInterface::class); $request->shouldReceive('getAttribute') - ->once() - ->with(CookieQueue::ATTRIBUTE, null) - ->andReturn(new CookieQueue()); + ->with(CookieQueue::ATTRIBUTE) + ->andReturn(new CookieQueue(), new CookieQueue(), new CookieQueue(), new CookieQueue()); $this->assertContainerBound(CookieQueue::class); + // Makes 3 calls to the container + $this->assertContainerBoundNotAsSingleton(CookieQueue::class, CookieQueue::class); } - public function testCookieQueueBindingShouldThrowAndExceptionWhenAttributeIsEmpty(): void + #[TestScope(Spiral::Http)] + public function testCookieQueueBindingWithoutCookieQueueInRequest(): void { - $this->expectExceptionMessage('Unable to resolve CookieQueue, invalid request scope'); $request = $this->mockContainer(ServerRequestInterface::class); $request->shouldReceive('getAttribute') - ->once() - ->with(CookieQueue::ATTRIBUTE, null) - ->andReturnNull(); + ->with(CookieQueue::ATTRIBUTE) + ->andReturn(null); - $this->assertContainerBound(CookieQueue::class); + $this->expectException(ContextualObjectNotFoundException::class); + + $this->getContainer()->get(CookieQueue::class); + } + + #[TestScope(Spiral::Http)] + public function testCookieQueueBindingWithoutRequest(): void + { + $this->expectException(InvalidRequestScopeException::class); + + $this->getContainer()->get(CookieQueue::class); } public function testConfig(): void @@ -51,7 +66,7 @@ public function testWhitelistCookie(): void $configs = new ConfigManager($this->createMock(LoaderInterface::class)); $configs->setDefaults(CookiesConfig::CONFIG, ['excluded' => []]); - $bootloader = new CookiesBootloader($configs); + $bootloader = new CookiesBootloader($configs, $this->getContainer()); $bootloader->whitelistCookie('foo'); $this->assertSame(['foo'], $configs->getConfig(CookiesConfig::CONFIG)['excluded']); diff --git a/tests/Framework/Bootloader/Http/HttpAuthBootloaderTest.php b/tests/Framework/Bootloader/Http/HttpAuthBootloaderTest.php index cbd1e86b0..f5dc61f9e 100644 --- a/tests/Framework/Bootloader/Http/HttpAuthBootloaderTest.php +++ b/tests/Framework/Bootloader/Http/HttpAuthBootloaderTest.php @@ -4,6 +4,10 @@ namespace Framework\Bootloader\Http; +use Psr\Http\Message\ServerRequestInterface; +use Spiral\Auth\ActorProviderInterface; +use Spiral\Auth\AuthContext; +use Spiral\Auth\AuthContextInterface; use Spiral\Auth\Config\AuthConfig; use Spiral\Auth\Session\TokenStorage; use Spiral\Auth\Session\TokenStorage as SessionTokenStorage; @@ -13,6 +17,9 @@ use Spiral\Bootloader\Auth\HttpAuthBootloader; use Spiral\Config\LoaderInterface; use Spiral\Config\ConfigManager; +use Spiral\Framework\Spiral; +use Spiral\Http\CurrentRequest; +use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Framework\BaseTestCase; final class HttpAuthBootloaderTest extends BaseTestCase @@ -27,6 +34,26 @@ public function testTokenStorageInterfaceBinding(): void $this->assertContainerBoundAsSingleton(TokenStorageInterface::class, TokenStorage::class); } + public function testProxyAuthContextInterfaceBinding(): void + { + $this->assertContainerBound(AuthContextInterface::class, AuthContextInterface::class); + } + + #[TestScope(Spiral::Http)] + public function testAuthContextInterfaceBinding(): void + { + $request = $this->createMock(ServerRequestInterface::class); + $request + ->method('getAttribute') + ->willReturn(new AuthContext($this->createMock(ActorProviderInterface::class))); + + $currentRequest = new CurrentRequest(); + $currentRequest->set($request); + $this->getContainer()->bindSingleton(CurrentRequest::class, $currentRequest); + + $this->assertContainerBound(AuthContextInterface::class, AuthContext::class); + } + public function testConfig(): void { $this->assertConfigHasFragments(AuthConfig::CONFIG, [ @@ -43,7 +70,7 @@ public function testAddTokenStorage(): void $configs = new ConfigManager($this->createMock(LoaderInterface::class)); $configs->setDefaults(AuthConfig::CONFIG, ['storages' => []]); - $bootloader = new HttpAuthBootloader($configs); + $bootloader = new HttpAuthBootloader($configs, $this->getContainer()); $bootloader->addTokenStorage('foo', 'bar'); $this->assertSame(['foo' => 'bar'], $configs->getConfig(AuthConfig::CONFIG)['storages']); @@ -54,7 +81,7 @@ public function testAddTransport(): void $configs = new ConfigManager($this->createMock(LoaderInterface::class)); $configs->setDefaults(AuthConfig::CONFIG, ['transports' => []]); - $bootloader = new HttpAuthBootloader($configs); + $bootloader = new HttpAuthBootloader($configs, $this->getContainer()); $bootloader->addTransport('foo', 'bar'); $this->assertSame(['foo' => 'bar'], $configs->getConfig(AuthConfig::CONFIG)['transports']); diff --git a/tests/Framework/Bootloader/Http/SessionBootloaderTest.php b/tests/Framework/Bootloader/Http/SessionBootloaderTest.php index 1630fb8e0..5a1f6719c 100644 --- a/tests/Framework/Bootloader/Http/SessionBootloaderTest.php +++ b/tests/Framework/Bootloader/Http/SessionBootloaderTest.php @@ -4,14 +4,22 @@ namespace Framework\Bootloader\Http; +use Spiral\Framework\Spiral; use Spiral\Session\SessionFactory; use Spiral\Session\SessionFactoryInterface; +use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Framework\BaseTestCase; final class SessionBootloaderTest extends BaseTestCase { + #[TestScope(Spiral::Http)] public function testSessionFactoryInterfaceBinding(): void { $this->assertContainerBoundAsSingleton(SessionFactoryInterface::class, SessionFactory::class); } + + public function testSessionFactoryInterfaceBindingInRootScope(): void + { + $this->assertContainerBoundAsSingleton(SessionFactoryInterface::class, SessionFactoryInterface::class); + } } diff --git a/tests/Framework/Bootloader/Router/RouterBootloaderTest.php b/tests/Framework/Bootloader/Router/RouterBootloaderTest.php index 2d31be257..bf70b1e54 100644 --- a/tests/Framework/Bootloader/Router/RouterBootloaderTest.php +++ b/tests/Framework/Bootloader/Router/RouterBootloaderTest.php @@ -7,6 +7,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Spiral\Core\CoreInterface; +use Spiral\Framework\Spiral; use Spiral\Router\GroupRegistry; use Spiral\Router\Loader\Configurator\RoutingConfigurator; use Spiral\Router\Loader\DelegatingLoader; @@ -18,6 +19,7 @@ use Spiral\Router\RouteInterface; use Spiral\Router\Router; use Spiral\Router\RouterInterface; +use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Framework\BaseTestCase; final class RouterBootloaderTest extends BaseTestCase @@ -62,6 +64,7 @@ public function testRoutePatternRegistryBinding(): void $this->assertContainerBoundAsSingleton(RoutePatternRegistryInterface::class, DefaultPatternRegistry::class); } + #[TestScope(Spiral::HttpRequest)] public function testRouteInterfaceBinding(): void { $request = $this->mockContainer(ServerRequestInterface::class); @@ -73,6 +76,7 @@ public function testRouteInterfaceBinding(): void $this->assertContainerBoundAsSingleton(RouteInterface::class, RouteInterface::class); } + #[TestScope(Spiral::HttpRequest)] public function testRouteInterfaceShouldThrowAnExceptionWhenRequestDoesNotContainIt(): void { $this->expectExceptionMessage('Unable to resolve Route, invalid request scope'); diff --git a/tests/Framework/Bootloader/Security/FiltersBootloaderTest.php b/tests/Framework/Bootloader/Security/FiltersBootloaderTest.php index fb9d1f3d7..a9daf92cb 100644 --- a/tests/Framework/Bootloader/Security/FiltersBootloaderTest.php +++ b/tests/Framework/Bootloader/Security/FiltersBootloaderTest.php @@ -15,6 +15,8 @@ use Spiral\Filters\Model\Interceptor\PopulateDataFromEntityInterceptor; use Spiral\Filters\Model\Interceptor\ValidateFilterInterceptor; use Spiral\Filters\InputInterface; +use Spiral\Framework\Spiral; +use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Framework\BaseTestCase; final class FiltersBootloaderTest extends BaseTestCase @@ -24,6 +26,7 @@ public function testFilterProviderInterfaceBinding(): void $this->assertContainerBoundAsSingleton(FilterProviderInterface::class, FilterProvider::class); } + #[TestScope(Spiral::HttpRequest)] public function testInputInterfaceBinding(): void { $this->assertContainerBoundAsSingleton(InputInterface::class, InputScope::class); diff --git a/tests/Framework/Filter/InputScopeTest.php b/tests/Framework/Filter/InputScopeTest.php index 0eea268c8..be19ada69 100644 --- a/tests/Framework/Filter/InputScopeTest.php +++ b/tests/Framework/Filter/InputScopeTest.php @@ -5,23 +5,25 @@ namespace Framework\Filter; use Nyholm\Psr7\ServerRequest; -use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UriInterface; -use Spiral\Core\Container; -use Spiral\Filter\InputScope; -use Spiral\Http\Request\InputManager; - -final class InputScopeTest extends TestCase +use Spiral\Filters\InputInterface; +use Spiral\Framework\Spiral; +use Spiral\Http\Config\HttpConfig; +use Spiral\Http\Request\InputBag; +use Spiral\Testing\Attribute\TestScope; +use Spiral\Tests\Framework\BaseTestCase; + +#[TestScope(Spiral::HttpRequest)] +final class InputScopeTest extends BaseTestCase { - private InputScope $input; private ServerRequestInterface $request; protected function setUp(): void { parent::setUp(); - $container = new Container(); $request = new ServerRequest( method: 'POST', uri: 'https://site.com/users', @@ -37,7 +39,7 @@ protected function setUp(): void ] ); - $container->bind( + $this->getContainer()->bind( ServerRequestInterface::class, $this->request = $request ->withQueryParams(['foo' => 'bar']) @@ -45,23 +47,21 @@ protected function setUp(): void ->withParsedBody(['quux' => 'corge']) ->withAttribute('foz', 'baf'), ); - - $this->input = new InputScope(new InputManager($container)); } public function testGetsMethod(): void { - $this->assertSame('POST', $this->input->getValue('method')); + $this->assertSame('POST', $this->getContainer()->get(InputInterface::class)->getValue('method')); } public function testGetsPath(): void { - $this->assertSame('/users', $this->input->getValue('path')); + $this->assertSame('/users', $this->getContainer()->get(InputInterface::class)->getValue('path')); } public function testGetsUri(): void { - $uri = $this->input->getValue('uri'); + $uri = $this->getContainer()->get(InputInterface::class)->getValue('uri'); $this->assertInstanceOf(UriInterface::class, $uri); $this->assertSame('https://site.com/users', (string)$uri); @@ -69,45 +69,57 @@ public function testGetsUri(): void public function testGetsRequest(): void { - $this->assertSame($this->request, $this->input->getValue('request')); + $this->assertSame($this->request, $this->getContainer()->get(InputInterface::class)->getValue('request')); } public function testGetsBearerToken(): void { - $this->assertSame('123', $this->input->getValue('bearerToken')); + $this->assertSame('123', $this->getContainer()->get(InputInterface::class)->getValue('bearerToken')); } public function testIsSecure(): void { - $this->assertTrue($this->input->getValue('isSecure')); + $this->assertTrue($this->getContainer()->get(InputInterface::class)->getValue('isSecure')); } public function testIsAjax(): void { - $this->assertTrue($this->input->getValue('isAjax')); + $this->assertTrue($this->getContainer()->get(InputInterface::class)->getValue('isAjax')); } public function testIsXmlHttpRequest(): void { - $this->assertTrue($this->input->getValue('isXmlHttpRequest')); + $this->assertTrue($this->getContainer()->get(InputInterface::class)->getValue('isXmlHttpRequest')); } public function testIsJsonExpected(): void { - $this->assertTrue($this->input->getValue('isJsonExpected', true)); + $this->assertTrue($this->getContainer()->get(InputInterface::class)->getValue('isJsonExpected', true)); } public function testGetsRemoteAddress(): void { - $this->assertSame('123.123.123', $this->input->getValue('remoteAddress')); + $this->assertSame('123.123.123', $this->getContainer()->get(InputInterface::class)->getValue('remoteAddress')); } - /** - * @dataProvider InputBagsDataProvider - */ + #[DataProvider('InputBagsDataProvider')] public function testGetsInputBag(string $source, string $name, mixed $expected): void { - $this->assertSame($expected, $this->input->getValue($source, $name)); + $this->assertSame($expected, $this->getContainer()->get(InputInterface::class)->getValue($source, $name)); + } + + public function testGetValueFromCustomInputBag(): void + { + $this->getContainer() + ->bind( + HttpConfig::class, + new HttpConfig(['inputBags' => ['test' => ['class' => InputBag::class, 'source' => 'getParsedBody']]]) + ); + + $this->assertSame( + 'corge', + $this->getContainer()->get(InputInterface::class)->getValue('test', 'quux') + ); } public static function InputBagsDataProvider(): \Traversable diff --git a/tests/Framework/Filter/Model/CastingErrorMessagesTest.php b/tests/Framework/Filter/Model/CastingErrorMessagesTest.php index c90633d98..6c8d00a7c 100644 --- a/tests/Framework/Filter/Model/CastingErrorMessagesTest.php +++ b/tests/Framework/Filter/Model/CastingErrorMessagesTest.php @@ -6,10 +6,13 @@ use Spiral\App\Request\CastingErrorMessages; use Spiral\Filters\Exception\ValidationException; +use Spiral\Framework\Spiral; +use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Framework\Filter\FilterTestCase; final class CastingErrorMessagesTest extends FilterTestCase { + #[TestScope(Spiral::HttpRequest)] public function testValidationMessages(): void { try { diff --git a/tests/Framework/Filter/Model/FilterWithSettersTest.php b/tests/Framework/Filter/Model/FilterWithSettersTest.php index 79ba75fae..e80088b4b 100644 --- a/tests/Framework/Filter/Model/FilterWithSettersTest.php +++ b/tests/Framework/Filter/Model/FilterWithSettersTest.php @@ -7,8 +7,11 @@ use Spiral\App\Request\FilterWithSetters; use Spiral\App\Request\PostFilter; use Spiral\Filters\Exception\ValidationException; +use Spiral\Framework\Spiral; +use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Framework\Filter\FilterTestCase; +#[TestScope(Spiral::HttpRequest)] final class FilterWithSettersTest extends FilterTestCase { public function testSetters(): void diff --git a/tests/Framework/Filter/Model/MethodAttributeTest.php b/tests/Framework/Filter/Model/MethodAttributeTest.php index 17c5bf70f..425b41ad4 100644 --- a/tests/Framework/Filter/Model/MethodAttributeTest.php +++ b/tests/Framework/Filter/Model/MethodAttributeTest.php @@ -5,10 +5,13 @@ namespace Framework\Filter\Model; use Spiral\App\Request\TestRequest; +use Spiral\Framework\Spiral; +use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Framework\Filter\FilterTestCase; final class MethodAttributeTest extends FilterTestCase { + #[TestScope(Spiral::HttpRequest)] public function testGetMethodValue(): void { $filter = $this->getFilter(TestRequest::class, method: 'GET'); diff --git a/tests/Framework/Filter/Model/NestedArrayFiltersTest.php b/tests/Framework/Filter/Model/NestedArrayFiltersTest.php index a899ca797..98569ddfe 100644 --- a/tests/Framework/Filter/Model/NestedArrayFiltersTest.php +++ b/tests/Framework/Filter/Model/NestedArrayFiltersTest.php @@ -8,8 +8,11 @@ use Spiral\App\Request\AddressFilter; use Spiral\App\Request\MultipleAddressesFilter; use Spiral\Filters\Exception\ValidationException; +use Spiral\Framework\Spiral; +use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Framework\Filter\FilterTestCase; +#[TestScope(Spiral::HttpRequest)] final class NestedArrayFiltersTest extends FilterTestCase { public function testGetsNestedFilter(): void diff --git a/tests/Framework/Filter/Model/NestedFilterTest.php b/tests/Framework/Filter/Model/NestedFilterTest.php index 6aaa03086..62160c94e 100644 --- a/tests/Framework/Filter/Model/NestedFilterTest.php +++ b/tests/Framework/Filter/Model/NestedFilterTest.php @@ -14,8 +14,11 @@ use Spiral\App\Request\WithNullableNestedFilter; use Spiral\App\Request\WithNullableRequiredNestedFilter; use Spiral\Filters\Exception\ValidationException; +use Spiral\Framework\Spiral; +use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Framework\Filter\FilterTestCase; +#[TestScope(Spiral::HttpRequest)] final class NestedFilterTest extends FilterTestCase { public function testGetsNestedFilter(): void diff --git a/tests/Framework/Http/AuthSessionTest.php b/tests/Framework/Http/AuthSessionTest.php index 9ad68bcb6..ee330a607 100644 --- a/tests/Framework/Http/AuthSessionTest.php +++ b/tests/Framework/Http/AuthSessionTest.php @@ -4,7 +4,11 @@ namespace Spiral\Tests\Framework\Http; +use Spiral\Auth\AuthContextInterface; +use Spiral\Bootloader\Http\Exception\ContextualObjectNotFoundException; +use Spiral\Bootloader\Http\Exception\InvalidRequestScopeException; use Spiral\Framework\Spiral; +use Spiral\Http\Config\HttpConfig; use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Framework\HttpTestCase; @@ -62,4 +66,25 @@ public function testLoginPayload(): void $this->fakeHttp()->get('/auth/token3', cookies: $result->getCookies())->assertBodySame('{"userID":1}'); } + + public function testInvalidSessionContextException(): void + { + $this->getContainer()->bind(HttpConfig::class, new HttpConfig([ + 'middleware' => [], + ])); + + $this->setHttpHandler(function (): void { + $this->expectException(ContextualObjectNotFoundException::class); + $this->getContainer()->get(AuthContextInterface::class); + }); + + $this->fakeHttp()->get('/'); + } + + public function testCookieQueueBindingWithoutRequest(): void + { + $this->expectException(InvalidRequestScopeException::class); + + $this->getContainer()->get(AuthContextInterface::class); + } } diff --git a/tests/Framework/Http/CookiesTest.php b/tests/Framework/Http/CookiesTest.php index a5af8e9d4..ec56143e1 100644 --- a/tests/Framework/Http/CookiesTest.php +++ b/tests/Framework/Http/CookiesTest.php @@ -4,15 +4,17 @@ namespace Spiral\Tests\Framework\Http; +use Psr\Http\Message\ServerRequestInterface; use Spiral\Cookies\Cookie; use Spiral\Cookies\CookieManager; +use Spiral\Cookies\CookieQueue; +use Spiral\Core\ContainerScope; use Spiral\Core\Exception\ScopeException; use Spiral\Encrypter\EncrypterInterface; use Spiral\Framework\Spiral; use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Framework\HttpTestCase; -#[TestScope(Spiral::Http)] final class CookiesTest extends HttpTestCase { public const ENV = [ @@ -38,16 +40,49 @@ public function testOutsideOfScopeFail(): void $this->cookies()->get('name'); } + #[TestScope([Spiral::Http, Spiral::HttpRequest])] + public function testCookieQueueInScope(): void + { + $this->setHttpHandler(static function (ServerRequestInterface $request) { + self::assertInstanceOf( + CookieQueue::class, + ContainerScope::getContainer()->get(ServerRequestInterface::class)->getAttribute(CookieQueue::ATTRIBUTE) + ); + + self::assertSame( + ContainerScope::getContainer() + ->get(ServerRequestInterface::class) + ->getAttribute(CookieQueue::ATTRIBUTE), + $request->getAttribute(CookieQueue::ATTRIBUTE) + ); + + self::assertSame( + ContainerScope::getContainer() + ->get(ServerRequestInterface::class) + ->getAttribute(CookieQueue::ATTRIBUTE), + ContainerScope::getContainer()->get(CookieQueue::class) + ); + }); + + $this->fakeHttp()->get('/')->assertOk(); + } + + #[TestScope([Spiral::Http, Spiral::HttpRequest])] public function testHasCookie(): void { - $this->setHttpHandler(fn (): int => (int)$this->cookies()->has('a')); + $this->setHttpHandler(function (ServerRequestInterface $request) { + return (int)$this->cookies()->has('a'); + }); $this->fakeHttp()->get('/')->assertOk()->assertBodySame('0'); } + #[TestScope([Spiral::Http, Spiral::HttpRequest])] public function testHasCookie2(): void { - $this->setHttpHandler(fn (): int => (int)$this->cookies()->has('a')); + $this->setHttpHandler(function (ServerRequestInterface $request) { + return (int)$this->cookies()->has('a'); + }); $this ->fakeHttp() @@ -59,9 +94,12 @@ public function testHasCookie2(): void ->assertBodySame('1'); } + #[TestScope([Spiral::Http, Spiral::HttpRequest])] public function testGetCookie2(): void { - $this->setHttpHandler(fn (): string => $this->cookies()->get('a')); + $this->setHttpHandler(function (ServerRequestInterface $request) { + return $this->cookies()->get('a'); + }); $this ->fakeHttp() @@ -73,9 +111,10 @@ public function testGetCookie2(): void ->assertBodySame('hello'); } + #[TestScope([Spiral::Http, Spiral::HttpRequest])] public function testSetCookie(): void { - $this->setHttpHandler(function (): string { + $this->setHttpHandler(function (ServerRequestInterface $request) { $this->cookies()->set('a', 'value'); return 'ok'; }); @@ -90,9 +129,10 @@ public function testSetCookie(): void ); } + #[TestScope([Spiral::Http, Spiral::HttpRequest])] public function testSetCookie2(): void { - $this->setHttpHandler(function (): string { + $this->setHttpHandler(function (ServerRequestInterface $request): string { $this->cookies()->schedule(Cookie::create('a', 'value')); $this->assertSame([], $this->cookies()->getAll()); $this->assertCount(1, $this->cookies()->getScheduled()); @@ -110,9 +150,10 @@ public function testSetCookie2(): void ); } + #[TestScope([Spiral::Http, Spiral::HttpRequest])] public function testDeleteCookie(): void { - $this->setHttpHandler(function (): string { + $this->setHttpHandler(function (ServerRequestInterface $request): string { $this->cookies()->delete('cookie'); return 'ok'; }); diff --git a/tests/Framework/Http/CurrentRequestTest.php b/tests/Framework/Http/CurrentRequestTest.php new file mode 100644 index 000000000..49d760a9e --- /dev/null +++ b/tests/Framework/Http/CurrentRequestTest.php @@ -0,0 +1,106 @@ +setHttpHandler(function (ServerRequestInterface $request): void { + $this->assertSame($request, $this->getContainer()->get(CurrentRequest::class)->get()); + }); + + $this->fakeHttp()->get('/')->assertOk(); + } + + public function testAttributesAddedInMiddlewareAreAccessibleInHandler(): void + { + $middleware1 = $this->createMiddleware('first', 'first-value'); + $middleware2 = $this->createMiddleware('second', 'second-value'); + + $this->getContainer()->bind(HttpConfig::class, new HttpConfig([ + 'middleware' => [ + $middleware1::class, + $middleware2::class, + ], + 'basePath' => '/', + 'headers' => [] + ])); + + $this->setHttpHandler(function (ServerRequestInterface $request): void { + $this->assertSame($request, $this->getContainer()->get(CurrentRequest::class)->get()); + }); + + $this->fakeHttp()->get('/')->assertOk(); + } + + public function testAttributesAddedInMiddlewareAreAccessibleInNextMiddleware(): void + { + $middleware1 = $this->createMiddleware('first', 5); + $middleware2 = new class($this->getContainer()) implements MiddlewareInterface { + public function __construct( + private readonly ContainerInterface $container, + ) { + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + $initial = $this->container->get(CurrentRequest::class)->get(); + + return $handler->handle($request->withAttribute('second', $initial->getAttribute('first') + 5)); + } + }; + $this->getContainer()->bind($middleware2::class, $middleware2); + + $this->getContainer()->bind(HttpConfig::class, new HttpConfig([ + 'middleware' => [ + $middleware1::class, + $middleware2::class, + ], + 'basePath' => '/', + 'headers' => [] + ])); + + $this->setHttpHandler(function (ServerRequestInterface $request): void { + $current = $this->getContainer()->get(CurrentRequest::class)->get(); + + $this->assertSame($request, $current); + $this->assertSame(5, $current->getAttribute('first')); + $this->assertSame(10, $current->getAttribute('second')); + }); + + $this->fakeHttp()->get('/')->assertOk(); + } + + private function createMiddleware(string $attribute, mixed $value): MiddlewareInterface + { + $middleware = new class($attribute, $value) implements MiddlewareInterface { + public function __construct( + private readonly string $attribute, + private readonly mixed $value, + ) { + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + return $handler->handle($request->withAttribute($this->attribute, $this->value)); + } + }; + + $this->getContainer()->bind($middleware::class, $middleware); + + return $middleware; + } +} diff --git a/tests/Framework/Http/SessionTest.php b/tests/Framework/Http/SessionTest.php index 367e3a12c..595ed40d7 100644 --- a/tests/Framework/Http/SessionTest.php +++ b/tests/Framework/Http/SessionTest.php @@ -4,7 +4,10 @@ namespace Spiral\Tests\Framework\Http; +use Spiral\Bootloader\Http\Exception\ContextualObjectNotFoundException; +use Spiral\Bootloader\Http\Exception\InvalidRequestScopeException; use Spiral\Framework\Spiral; +use Spiral\Http\Config\HttpConfig; use Spiral\Session\SessionInterface; use Spiral\Testing\Attribute\TestScope; use Spiral\Tests\Framework\HttpTestCase; @@ -102,6 +105,28 @@ public function testDestroySession(): void ->assertBodySame('1'); } + public function testInvalidSessionContextException(): void + { + $this->getContainer()->bind(HttpConfig::class, new HttpConfig([ + 'middleware' => [], + ])); + + $this->setHttpHandler(function (): void { + $this->expectException(ContextualObjectNotFoundException::class); + + $this->session(); + }); + + $this->fakeHttp()->get(uri: '/')->assertOk(); + } + + public function testSessionBindingWithoutRequest(): void + { + $this->expectException(InvalidRequestScopeException::class); + + $this->session(); + } + private function session(): SessionInterface { return $this->getContainer()->get(SessionInterface::class);