diff --git a/src/Framework/Bootloader/Http/HttpBootloader.php b/src/Framework/Bootloader/Http/HttpBootloader.php index 2f5046be0..78bf0225a 100644 --- a/src/Framework/Bootloader/Http/HttpBootloader.php +++ b/src/Framework/Bootloader/Http/HttpBootloader.php @@ -24,7 +24,7 @@ use Spiral\Http\Config\HttpConfig; use Spiral\Http\CurrentRequest; use Spiral\Http\Http; -use Spiral\Http\Pipeline; +use Spiral\Http\LazyPipeline; use Spiral\Telemetry\Bootloader\TelemetryBootloader; use Spiral\Telemetry\TracerFactoryInterface; @@ -136,7 +136,7 @@ public function addInputBag(string $bag, array $config): void */ protected function httpCore( HttpConfig $config, - Pipeline $pipeline, + LazyPipeline $pipeline, RequestHandlerInterface $handler, ResponseFactoryInterface $responseFactory, ContainerInterface $container, diff --git a/src/Framework/Bootloader/Http/RoutesBootloader.php b/src/Framework/Bootloader/Http/RoutesBootloader.php index f52315376..f4af1a828 100644 --- a/src/Framework/Bootloader/Http/RoutesBootloader.php +++ b/src/Framework/Bootloader/Http/RoutesBootloader.php @@ -8,6 +8,8 @@ use Spiral\Boot\Bootloader\Bootloader; use Spiral\Core\BinderInterface; use Spiral\Core\Container\Autowire; +use Spiral\Core\FactoryInterface; +use Spiral\Http\LazyPipeline; use Spiral\Http\Pipeline; use Spiral\Router\GroupRegistry; use Spiral\Router\Loader\Configurator\RoutingConfigurator; @@ -60,12 +62,14 @@ abstract protected function middlewareGroups(): array; private function registerMiddlewareGroups(BinderInterface $binder, array $groups): void { foreach ($groups as $group => $middleware) { - $binder->bind( - 'middleware:' . $group, - static function (PipelineFactory $factory) use ($middleware): Pipeline { - return $factory->createWithMiddleware($middleware); - } - ); + $binder + ->getBinder('http') + ->bind( + 'middleware:' . $group, + static function (FactoryInterface $factory) use ($middleware): LazyPipeline { + return $factory->make(LazyPipeline::class)->withAddedMiddleware(...$middleware); + } + ); } } diff --git a/src/Http/src/CurrentRequest.php b/src/Http/src/CurrentRequest.php index 4feee69ca..244c5e004 100644 --- a/src/Http/src/CurrentRequest.php +++ b/src/Http/src/CurrentRequest.php @@ -6,7 +6,6 @@ use Psr\Http\Message\ServerRequestInterface; use Spiral\Core\Attribute\Scope; -use Spiral\Http\Exception\HttpException; /** * Provides access to the current request in the `http` scope. @@ -17,7 +16,7 @@ final class CurrentRequest { private ?ServerRequestInterface $request = null; - public function set(ServerRequestInterface $request): void + public function set(?ServerRequestInterface $request): void { $this->request = $request; } diff --git a/src/Http/src/Http.php b/src/Http/src/Http.php index 374428775..6105904ab 100644 --- a/src/Http/src/Http.php +++ b/src/Http/src/Http.php @@ -25,24 +25,35 @@ final class Http implements RequestHandlerInterface { private ?RequestHandlerInterface $handler = null; private readonly TracerFactoryInterface $tracerFactory; + private readonly Pipeline|LazyPipeline $pipeline; public function __construct( private readonly HttpConfig $config, - private readonly Pipeline $pipeline, + Pipeline|LazyPipeline $pipeline, private readonly ResponseFactoryInterface $responseFactory, private readonly ContainerInterface $container, ?TracerFactoryInterface $tracerFactory = null, private readonly ?EventDispatcherInterface $dispatcher = null, ) { - foreach ($this->config->getMiddleware() as $middleware) { - $this->pipeline->pushMiddleware($this->container->get($middleware)); + if ($pipeline instanceof Pipeline) { + foreach ($this->config->getMiddleware() as $middleware) { + $pipeline->pushMiddleware($this->container->get($middleware)); + } + } else { + $pipeline = $pipeline->withAddedMiddleware( + ...$this->config->getMiddleware() + ); } + $this->pipeline = $pipeline; $scope = $this->container instanceof ScopeInterface ? $this->container : new Container(); $this->tracerFactory = $tracerFactory ?? new NullTracerFactory($scope); } - public function getPipeline(): Pipeline + /** + * @internal + */ + public function getPipeline(): Pipeline|LazyPipeline { return $this->pipeline; } @@ -97,7 +108,10 @@ public function handle(ServerRequestInterface $request): ResponseInterface attributes: [ 'http.method' => $request->getMethod(), 'http.url' => (string) $request->getUri(), - 'http.headers' => $request->getHeaders(), + 'http.headers' => \array_map( + static fn (array $values): string => \implode(',', $values), + $request->getHeaders(), + ), ], scoped: true, traceKind: TraceKind::SERVER, diff --git a/src/Http/src/LazyPipeline.php b/src/Http/src/LazyPipeline.php new file mode 100644 index 000000000..b2576c40e --- /dev/null +++ b/src/Http/src/LazyPipeline.php @@ -0,0 +1,149 @@ + + */ + protected array $middleware = []; + private ?RequestHandlerInterface $handler = null; + private int $position = 0; + /** + * Trace span for the current pipeline run. + */ + private ?SpanInterface $span = null; + + public function __construct( + #[Proxy] private readonly ContainerInterface $container, + private readonly ?EventDispatcherInterface $dispatcher = null, + ) { + } + + /** + * Add middleware to the pipeline. + * + * @param MiddlewareInterface ...$middleware List of middleware or its definition. + */ + public function withAddedMiddleware(MiddlewareInterface|Autowire|string ...$middleware): self + { + $pipeline = clone $this; + $pipeline->middleware = \array_merge($pipeline->middleware, $middleware); + return $pipeline; + } + + /** + * Replace middleware in the pipeline. + * + * @param MiddlewareInterface ...$middleware List of middleware or its definition. + */ + public function withMiddleware(MiddlewareInterface|Autowire|string ...$middleware): self + { + $pipeline = clone $this; + $pipeline->middleware = $middleware; + return $pipeline; + } + + /** + * Configures pipeline with target endpoint. + * + * @throws PipelineException + */ + public function withHandler(RequestHandlerInterface $handler): self + { + $pipeline = clone $this; + $pipeline->handler = $handler; + return $pipeline; + } + + public function process(Request $request, RequestHandlerInterface $handler): Response + { + return $this->withHandler($handler)->handle($request); + } + + public function handle(Request $request): Response + { + $this->handler === null and throw new PipelineException('Unable to run pipeline, no handler given.'); + + /** @var CurrentRequest $currentRequest */ + $currentRequest = $this->container->get(CurrentRequest::class); + + $previousRequest = $currentRequest->get(); + $currentRequest->set($request); + try { + // There is no middleware to process, let's pass the request to the handler + if (!\array_key_exists($this->position, $this->middleware)) { + return $this->handler->handle($request); + } + + $middleware = $this->resolveMiddleware($this->position); + $this->dispatcher?->dispatch(new MiddlewareProcessing($request, $middleware)); + + $span = $this->span; + + $middlewareTitle = \is_string($this->middleware[$this->position]) + && $this->middleware[$this->position] !== $middleware::class + ? \sprintf('%s=%s', $this->middleware[$this->position], $middleware::class) + : $middleware::class; + // Init a tracing span when the pipeline starts + if ($span === null) { + /** @var TracerInterface $tracer */ + $tracer = $this->container->get(TracerInterface::class); + return $tracer->trace( + name: 'HTTP Pipeline', + callback: function (SpanInterface $span) use ($request, $middleware, $middlewareTitle): Response { + $span->setAttribute('http.middleware', [$middlewareTitle]); + return $middleware->process($request, $this->next($span)); + }, + scoped: true, + ); + } + + $middlewares = $span->getAttribute('http.middleware') ?? []; + $middlewares[] = $middlewareTitle; + $span->setAttribute('http.middleware', $middlewares); + + return $middleware->process($request, $this->next($span)); + } finally { + $currentRequest->set($previousRequest); + } + } + + private function next(SpanInterface $span): self + { + $pipeline = clone $this; + ++$pipeline->position; + $pipeline->span = $span; + return $pipeline; + } + + private function resolveMiddleware(int $position): MiddlewareInterface + { + $middleware = $this->middleware[$position]; + return $middleware instanceof MiddlewareInterface + ? $middleware + : $this->container->get($middleware); + } +} diff --git a/src/Http/src/Pipeline.php b/src/Http/src/Pipeline.php index 79dc91ae1..38259e09e 100644 --- a/src/Http/src/Pipeline.php +++ b/src/Http/src/Pipeline.php @@ -21,6 +21,7 @@ /** * Pipeline used to pass request and response thought the chain of middleware. + * @deprecated Will be removed in v4.0. Use {@see LazyPipeline} instead. */ final class Pipeline implements RequestHandlerInterface, MiddlewareInterface { @@ -31,7 +32,7 @@ final class Pipeline implements RequestHandlerInterface, MiddlewareInterface private ?RequestHandlerInterface $handler = null; public function __construct( - #[Proxy] private readonly ScopeInterface $scope, + #[Proxy] ScopeInterface $scope, private readonly ?EventDispatcherInterface $dispatcher = null, ?TracerInterface $tracer = null ) { diff --git a/src/Http/tests/HttpTest.php b/src/Http/tests/HttpTest.php index 72a17b632..90b52ac03 100644 --- a/src/Http/tests/HttpTest.php +++ b/src/Http/tests/HttpTest.php @@ -283,7 +283,7 @@ public function testTraceAttributesAreSet(): void [ 'http.method' => 'GET', 'http.url' => 'http://example.org/path', - 'http.headers' => ['Host' => ['example.org'], 'foo' => ['bar']], + 'http.headers' => ['Host' => 'example.org', 'foo' => 'bar'], ], true, TraceKind::SERVER, @@ -293,7 +293,7 @@ function ($name, $callback, $attributes, $scoped, $traceKind) { self::assertSame($attributes, [ 'http.method' => 'GET', 'http.url' => 'http://example.org/path', - 'http.headers' => ['Host' => ['example.org'], 'foo' => ['bar']], + 'http.headers' => ['Host' => 'example.org', 'foo' => 'bar'], ]); return $this->container ->get(TracerInterface::class) diff --git a/src/Router/src/CoreHandler.php b/src/Router/src/CoreHandler.php index 1624ee7a2..35afa6636 100644 --- a/src/Router/src/CoreHandler.php +++ b/src/Router/src/CoreHandler.php @@ -6,6 +6,7 @@ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\RequestHandlerInterface; use Spiral\Core\CoreInterface; @@ -124,6 +125,9 @@ public function handle(Request $request): Response new CallContext( Target::fromPair($controller, $action), $parameters, + [ + ServerRequestInterface::class => $request, + ], ), ), attributes: [ diff --git a/src/Router/src/Route.php b/src/Router/src/Route.php index 270c79f78..67cd6dc16 100644 --- a/src/Router/src/Route.php +++ b/src/Router/src/Route.php @@ -13,6 +13,7 @@ use Spiral\Http\CallableHandler; use Spiral\Router\Exception\RouteException; use Spiral\Router\Exception\TargetException; +use Spiral\Router\Traits\LazyPipelineTrait; use Spiral\Router\Traits\PipelineTrait; /** @@ -90,7 +91,7 @@ public function withContainer(ContainerInterface $container): ContainerizedInter $route->target = clone $route->target; } - $route->pipeline = $route->makePipeline(); + $route->pipeline = $route->makeLazyPipeline(); return $route; } @@ -129,9 +130,9 @@ public function handle(ServerRequestInterface $request): ResponseInterface */ protected function requestHandler(): RequestHandlerInterface { - if (!$this->hasContainer()) { - throw new RouteException('Unable to configure route pipeline without associated container'); - } + $this->hasContainer() or throw new RouteException( + 'Unable to configure route pipeline without associated container.', + ); if ($this->target instanceof TargetInterface) { try { diff --git a/src/Router/src/Traits/PipelineTrait.php b/src/Router/src/Traits/PipelineTrait.php index dd43f8067..32c5ad715 100644 --- a/src/Router/src/Traits/PipelineTrait.php +++ b/src/Router/src/Traits/PipelineTrait.php @@ -7,6 +7,7 @@ use Psr\Container\ContainerExceptionInterface; use Psr\Http\Server\MiddlewareInterface; use Spiral\Core\Container\Autowire; +use Spiral\Http\LazyPipeline; use Spiral\Http\Pipeline; use Spiral\Router\Exception\RouteException; use Spiral\Router\PipelineFactory; @@ -19,7 +20,7 @@ trait PipelineTrait { use ContainerTrait; - protected ?Pipeline $pipeline = null; + protected Pipeline|LazyPipeline|null $pipeline = null; /** @psalm-var array */ protected array $middleware = []; @@ -53,7 +54,7 @@ public function withMiddleware(...$middleware): RouteInterface } if ($route->pipeline !== null) { - $route->pipeline = $route->makePipeline(); + $route->pipeline = $route->makeLazyPipeline(); } return $route; @@ -73,6 +74,8 @@ public function withPipeline(Pipeline $pipeline): static * Get associated route pipeline. * * @throws RouteException + * + * @deprecated Will be removed in Spiral v4.0. Use {@see makeLazyPipeline()} instead. */ protected function makePipeline(): Pipeline { @@ -85,4 +88,21 @@ protected function makePipeline(): Pipeline throw new RouteException($e->getMessage(), $e->getCode(), $e); } } + + /** + * Get associated route pipeline. + * + * @throws RouteException + */ + protected function makeLazyPipeline(): LazyPipeline + { + \assert($this->container !== null); + try { + /** @var LazyPipeline $pipeline */ + $pipeline = $this->container->get(LazyPipeline::class); + return $pipeline->withMiddleware(...$this->middleware); + } catch (ContainerExceptionInterface $e) { + throw new RouteException($e->getMessage(), $e->getCode(), $e); + } + } } diff --git a/src/Router/tests/BaseTestCase.php b/src/Router/tests/BaseTestCase.php index 02083e3b8..48ba55003 100644 --- a/src/Router/tests/BaseTestCase.php +++ b/src/Router/tests/BaseTestCase.php @@ -37,6 +37,11 @@ abstract class BaseTestCase extends TestCase protected Container $container; protected Router $router; + protected function getContainer(): Container + { + return $this->container; + } + protected function setUp(): void { $this->initContainer(); @@ -49,11 +54,11 @@ protected function makeRouter(string $basePath = '', ?EventDispatcherInterface $ $basePath, new UriHandler( new UriFactory(), - new Slugify() + new Slugify(), ), $this->container, $dispatcher, - new NullTracer($this->container) + new NullTracer($this->container), ); } @@ -88,8 +93,8 @@ private function initContainer(): void new LoaderRegistry([ new PhpFileLoader($this->container, $this->container), new TestLoader(), - ]) - ) + ]), + ), ); $this->container->bind(HandlerInterface::class, Core::class); diff --git a/src/Router/tests/BaseTestingCase.php b/src/Router/tests/BaseTestingCase.php new file mode 100644 index 000000000..3eb4e5824 --- /dev/null +++ b/src/Router/tests/BaseTestingCase.php @@ -0,0 +1,40 @@ +router = $this->getContainer()->get(RouterInterface::class); + } + + /** + * @throws \ReflectionException + */ + protected function getProperty(object $object, string $property): mixed + { + $r = new \ReflectionObject($object); + + return $r->getProperty($property)->getValue($object); + } +} diff --git a/src/Router/tests/ContainerScopeTest.php b/src/Router/tests/ContainerScopeTest.php index c74cd6647..b2bf9c7bb 100644 --- a/src/Router/tests/ContainerScopeTest.php +++ b/src/Router/tests/ContainerScopeTest.php @@ -15,7 +15,7 @@ use Spiral\Tests\Router\Fixtures\UserContextController; use Spiral\Tests\Router\Stub\IdentityScopedMiddleware; -class ContainerScopeTest extends \Spiral\Testing\TestCase +class ContainerScopeTest extends BaseTestingCase { public function defineBootloaders(): array { diff --git a/src/Router/tests/ControllerTest.php b/src/Router/tests/ControllerTest.php index 400570822..79343ccba 100644 --- a/src/Router/tests/ControllerTest.php +++ b/src/Router/tests/ControllerTest.php @@ -74,10 +74,10 @@ public function testOptionalParam(): void public function testFallbackHandler(): void { $target = new Action(TestController::class, 'default'); - $this->container->removeBinding(HandlerInterface::class); - $this->container->removeBinding(CoreInterface::class); + $this->getContainer()->removeBinding(HandlerInterface::class); + $this->getContainer()->removeBinding(CoreInterface::class); - $core = $target->getHandler($this->container, []); + $core = $target->getHandler($this->getContainer(), []); $handler = (fn(CoreHandler $core) => $core->core)->call($core, $core); self::assertInstanceOf(AutowireHandler::class, $handler); diff --git a/src/Router/tests/Fixtures/TestRouterBootloader.php b/src/Router/tests/Fixtures/TestRouterBootloader.php new file mode 100644 index 000000000..441126047 --- /dev/null +++ b/src/Router/tests/Fixtures/TestRouterBootloader.php @@ -0,0 +1,33 @@ + fn(Container $container) => new LoaderRegistry([ + new PhpFileLoader($container, $container), + new TestLoader(), + ]), + ]; + } +} diff --git a/src/Router/tests/Loader/Configurator/RoutingConfiguratorTest.php b/src/Router/tests/Loader/Configurator/RoutingConfiguratorTest.php index 3dc094f91..d347184d6 100644 --- a/src/Router/tests/Loader/Configurator/RoutingConfiguratorTest.php +++ b/src/Router/tests/Loader/Configurator/RoutingConfiguratorTest.php @@ -13,7 +13,7 @@ final class RoutingConfiguratorTest extends BaseTestCase { public function testImportWithoutConcreteLoader(): void { - $routes = $this->container->get(RoutingConfigurator::class); + $routes = $this->getContainer()->get(RoutingConfigurator::class); $this->assertCount(0, $routes->getCollection()); @@ -24,7 +24,7 @@ public function testImportWithoutConcreteLoader(): void public function testImportWithLoader(): void { - $routes = $this->container->get(RoutingConfigurator::class); + $routes = $this->getContainer()->get(RoutingConfigurator::class); $this->assertCount(0, $routes->getCollection()); @@ -35,7 +35,7 @@ public function testImportWithLoader(): void public function testImportWithWrongLoader(): void { - $routes = $this->container->get(RoutingConfigurator::class); + $routes = $this->getContainer()->get(RoutingConfigurator::class); $this->assertCount(0, $routes->getCollection()); @@ -46,14 +46,14 @@ public function testImportWithWrongLoader(): void public function testGetCollection(): void { - $routes = $this->container->get(RoutingConfigurator::class); + $routes = $this->getContainer()->get(RoutingConfigurator::class); $this->assertInstanceOf(RouteCollection::class, $routes->getCollection()); } public function testDefault(): void { - $routes = $this->container->get(RoutingConfigurator::class); + $routes = $this->getContainer()->get(RoutingConfigurator::class); $this->assertNull($routes->getDefault()); @@ -64,7 +64,7 @@ public function testDefault(): void public function testAdd(): void { - $routes = $this->container->get(RoutingConfigurator::class); + $routes = $this->getContainer()->get(RoutingConfigurator::class); $this->assertCount(0, $routes->getCollection()); $route = $routes->add('test', '/')->callable(static fn () => null); diff --git a/src/Router/tests/MiddlewareTest.php b/src/Router/tests/MiddlewareTest.php index aa114d21c..73d2301ea 100644 --- a/src/Router/tests/MiddlewareTest.php +++ b/src/Router/tests/MiddlewareTest.php @@ -6,6 +6,7 @@ use Nyholm\Psr7\ServerRequest; use Nyholm\Psr7\Uri; +use Psr\Container\NotFoundExceptionInterface; use Spiral\Router\Exception\RouteException; use Spiral\Router\Route; use Spiral\Router\Target\Group; @@ -14,8 +15,10 @@ use Spiral\Tests\Router\Fixtures\TestController; use Spiral\Tests\Router\Stub\HeaderMiddleware; -class MiddlewareTest extends BaseTestCase +class MiddlewareTest extends BaseTestingCase { + use RouterFactoryTrait; + public function testRoute(): void { $router = $this->makeRouter(); @@ -82,40 +85,28 @@ public function testRouteArray(): void public function testInvalid(): void { - $this->expectException(RouteException::class); - $router = $this->makeRouter(); + $this->expectException(\Throwable::class); $router->setRoute( 'group', (new Route('/[/[/]]', new Group([ 'test' => TestController::class, ])))->withMiddleware($this) ); - - $response = $router->handle(new ServerRequest('GET', new Uri('/test'))); - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('hello world', (string)$response->getBody()); - $this->assertSame('Value*, Value*', $response->getHeaderLine('Header')); } public function testInvalid2(): void { - $this->expectException(RouteException::class); - $router = $this->makeRouter(); + $this->expectException(\Throwable::class); $router->setRoute( 'group', (new Route('/[/[/]]', new Group([ 'test' => TestController::class, ])))->withMiddleware([[]]) ); - - $response = $router->handle(new ServerRequest('GET', new Uri('/test'))); - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('hello world', (string)$response->getBody()); - $this->assertSame('Value*, Value*', $response->getHeaderLine('Header')); } public function testPipelineException(): void @@ -134,18 +125,18 @@ public function testPipelineException(): void $this->assertSame('Value*, Value*', $response->getHeaderLine('Header')); } - public function testPipelineExceptionMiddleware(): void + public function testUndefinedMiddleware(): void { - $this->expectException(RouteException::class); - $r = (new Route('/[/[/]]', new Group([ 'test' => TestController::class, ])))->withMiddleware([new HeaderMiddleware(), 'other']); $r = $r->withUriHandler(new UriHandler(new UriFactory())); - $r = $r->withContainer($this->container); + $r = $r->withContainer($this->getContainer()); $r = $r->match(new ServerRequest('GET', new Uri('/test'))); + + $this->expectException(NotFoundExceptionInterface::class); $r->handle(new ServerRequest('GET', new Uri('/test'))); } } diff --git a/src/Router/tests/NamespacedTest.php b/src/Router/tests/NamespacedTest.php index 9583891f2..3f8036d5e 100644 --- a/src/Router/tests/NamespacedTest.php +++ b/src/Router/tests/NamespacedTest.php @@ -59,7 +59,7 @@ public function testBypass(): void $n = new Namespaced('Spiral\Tests\Router\Fixtures'); - $n->getHandler($this->container, [ + $n->getHandler($this->getContainer(), [ 'controller' => 'secret/controller', 'action' => null, ]); diff --git a/src/Router/tests/RegistryTest.php b/src/Router/tests/RegistryTest.php index e6ebb8115..b3f3129b3 100644 --- a/src/Router/tests/RegistryTest.php +++ b/src/Router/tests/RegistryTest.php @@ -14,9 +14,9 @@ final class RegistryTest extends BaseTestCase { public function testSameGroup(): void { - $registry = new GroupRegistry($this->container); - $router = new Router('/', new UriHandler(new Psr17Factory()), $this->container); - $this->container->bind(RouterInterface::class, $router); + $registry = new GroupRegistry($this->getContainer()); + $router = new Router('/', new UriHandler(new Psr17Factory()), $this->getContainer()); + $this->getContainer()->bind(RouterInterface::class, $router); $group = $registry->getGroup('default'); $this->assertSame($group, $registry->getGroup('default')); diff --git a/src/Router/tests/RouteGroupTest.php b/src/Router/tests/RouteGroupTest.php index c4bb000e2..53166a780 100644 --- a/src/Router/tests/RouteGroupTest.php +++ b/src/Router/tests/RouteGroupTest.php @@ -93,7 +93,8 @@ public function testMiddleware(mixed $middleware): void $m = $this->getProperty($p, 'middleware'); $this->assertCount(1, $m); - $this->assertInstanceOf(TestMiddleware::class, $m[0]); + // Because of the pipeline is lazy + $this->assertSame($middleware, $m[0]); } public function testRouteWithMiddlewareAddGroupMiddleware(): void @@ -112,8 +113,9 @@ public function testRouteWithMiddlewareAddGroupMiddleware(): void $this->assertCount(2, $m); - $this->assertInstanceOf(TestMiddleware::class, $m[1]); - $this->assertInstanceOf(AnotherMiddleware::class, $m[0]); + // Because of the pipeline is lazy + $this->assertSame(TestMiddleware::class, $m[1]); + $this->assertSame(AnotherMiddleware::class, $m[0]); } public function testWithoutNamePrefix(): void diff --git a/src/Router/tests/RouteTest.php b/src/Router/tests/RouteTest.php index fe8e8ebd2..c1a769e11 100644 --- a/src/Router/tests/RouteTest.php +++ b/src/Router/tests/RouteTest.php @@ -51,7 +51,8 @@ public function testWithMiddleware(mixed $middleware): void $m = $this->getProperty($p, 'middleware'); $this->assertCount(1, $m); - $this->assertInstanceOf(TestMiddleware::class, $m[0]); + // Because of the pipeline is lazy + $this->assertSame($middleware, $m[0]); } public static function prefixesDataProvider(): \Traversable diff --git a/src/Router/tests/RouterFactoryTrait.php b/src/Router/tests/RouterFactoryTrait.php new file mode 100644 index 000000000..01f2991c2 --- /dev/null +++ b/src/Router/tests/RouterFactoryTrait.php @@ -0,0 +1,34 @@ +getContainer(); + return new Router( + $basePath, + new UriHandler( + new UriFactory(), + new Slugify(), + ), + $container, + $dispatcher, + new NullTracer($container), + ); + } +} diff --git a/src/Router/tests/RouterTest.php b/src/Router/tests/RouterTest.php index 01ac370bd..23fc1f54c 100644 --- a/src/Router/tests/RouterTest.php +++ b/src/Router/tests/RouterTest.php @@ -48,7 +48,7 @@ public function testCastError(): void public function testEventsShouldBeDispatched(): void { $request = new ServerRequest('GET', '/foo'); - $route = (new Route('/foo', Call::class))->withContainer($this->container); + $route = (new Route('/foo', Call::class))->withContainer($this->getContainer()); $dispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher @@ -88,7 +88,7 @@ public function testImportWithHost(): void $configurator->add('foo', '///register')->callable(fn () => null); $router->import($configurator); - $this->container->get(GroupRegistry::class)->registerRoutes($router); + $this->getContainer()->get(GroupRegistry::class)->registerRoutes($router); $uri = (string) $router->uri('foo', ['host' => 'some']); $this->assertSame('some/register', $uri); diff --git a/src/SendIt/tests/RenderTest.php b/src/SendIt/tests/RenderTest.php index 2c33b993a..bd2a9a163 100644 --- a/src/SendIt/tests/RenderTest.php +++ b/src/SendIt/tests/RenderTest.php @@ -38,6 +38,8 @@ public function defineDirectories(string $root): array public function tearDown(): void { + parent::tearDown(); + foreach (glob(__DIR__ . '/App/runtime/cache/views/*.php') as $file) { @unlink($file); } diff --git a/src/Stempler/tests/TraverserTest.php b/src/Stempler/tests/TraverserTest.php index 884eff724..759f7ba01 100644 --- a/src/Stempler/tests/TraverserTest.php +++ b/src/Stempler/tests/TraverserTest.php @@ -118,7 +118,6 @@ public function leaveNode(mixed $node, VisitorContext $ctx): mixed ); } - #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] public function testVisitorContext(): void { $doc = $this->parse('hello'); @@ -127,6 +126,8 @@ public function testVisitorContext(): void $t->addVisitor($this); $doc->nodes = $t->traverse($doc->nodes); + // To suppress Rector error + $this->assertTrue(true); } public function enterNode(mixed $node, VisitorContext $ctx): mixed diff --git a/src/Telemetry/src/AbstractTracer.php b/src/Telemetry/src/AbstractTracer.php index 547e3ddab..f8c073d2f 100644 --- a/src/Telemetry/src/AbstractTracer.php +++ b/src/Telemetry/src/AbstractTracer.php @@ -4,8 +4,9 @@ namespace Spiral\Telemetry; -use Spiral\Core\Attribute\Proxy; +use Spiral\Core\BinderInterface; use Spiral\Core\Container; +use Spiral\Core\ContainerScope; use Spiral\Core\InvokerInterface; use Spiral\Core\ScopeInterface; @@ -17,7 +18,7 @@ abstract class AbstractTracer implements TracerInterface { public function __construct( - #[Proxy] private readonly ?ScopeInterface $scope = new Container(), + private readonly ?ScopeInterface $scope = new Container(), ) { } @@ -26,10 +27,37 @@ public function __construct( */ final protected function runScope(Span $span, callable $callback): mixed { - // TODO: Can we remove this scope? - return $this->scope->runScope([ - SpanInterface::class => $span, - TracerInterface::class => $this, - ], static fn (InvokerInterface $invoker): mixed => $invoker->invoke($callback)); + $container = ContainerScope::getContainer(); + if ($container === null) { + return $this->scope->runScope([ + SpanInterface::class => $span, + TracerInterface::class => $this, + ], static fn (InvokerInterface $invoker): mixed => $invoker->invoke($callback)); + } + + if ($container instanceof Container) { + $invoker = $container; + $binder = $container; + } else { + /** @var InvokerInterface $invoker */ + $invoker = $container->get(InvokerInterface::class); + /** @var BinderInterface $binder */ + $binder = $container->get(BinderInterface::class); + } + + try { + $prevSpan = $container->get(SpanInterface::class); + } catch (\Throwable) { + $prevSpan = null; + } + + $binder->bindSingleton(SpanInterface::class, $span); + try { + return $invoker->invoke($callback); + } finally { + $prevSpan === null + ? $binder->removeBinding(SpanInterface::class) + : $binder->bindSingleton(SpanInterface::class, $prevSpan); + } } } diff --git a/src/Telemetry/src/Bootloader/TelemetryBootloader.php b/src/Telemetry/src/Bootloader/TelemetryBootloader.php index d995cfe6d..2ceab13a2 100644 --- a/src/Telemetry/src/Bootloader/TelemetryBootloader.php +++ b/src/Telemetry/src/Bootloader/TelemetryBootloader.php @@ -14,13 +14,11 @@ use Spiral\Telemetry\Config\TelemetryConfig; use Spiral\Telemetry\ConfigTracerFactoryProvider; use Spiral\Telemetry\Exception\TracerException; -use Spiral\Telemetry\LogTracer; use Spiral\Telemetry\LogTracerFactory; -use Spiral\Telemetry\NullTracer; use Spiral\Telemetry\NullTracerFactory; use Spiral\Telemetry\TracerFactoryInterface; -use Spiral\Telemetry\TracerInterface; use Spiral\Telemetry\TracerFactoryProviderInterface; +use Spiral\Telemetry\TracerInterface; final class TelemetryBootloader extends Bootloader { @@ -35,7 +33,7 @@ final class TelemetryBootloader extends Bootloader ]; public function __construct( - private readonly ConfiguratorInterface $config + private readonly ConfiguratorInterface $config, ) { } @@ -51,7 +49,7 @@ public function registerTracer(string $name, string|TracerFactoryInterface|Autow { $this->config->modify( TelemetryConfig::CONFIG, - new Append('drivers', $name, $driver) + new Append('drivers', $name, $driver), ); } @@ -59,7 +57,7 @@ public function registerTracer(string $name, string|TracerFactoryInterface|Autow * @throws TracerException */ public function initFactory( - TracerFactoryProviderInterface $tracerProvider + TracerFactoryProviderInterface $tracerProvider, ): TracerFactoryInterface { return $tracerProvider->getTracerFactory(); } @@ -68,7 +66,7 @@ public function initFactory( * @throws TracerException */ public function getTracer( - TracerFactoryInterface $tracerFactory + TracerFactoryInterface $tracerFactory, ): TracerInterface { return $tracerFactory->make(); } @@ -83,7 +81,7 @@ private function initConfig(EnvironmentInterface $env): void 'null' => NullTracerFactory::class, 'log' => LogTracerFactory::class, ], - ] + ], ); } } diff --git a/src/Telemetry/src/LogTracerFactory.php b/src/Telemetry/src/LogTracerFactory.php index 4b63d1d8a..c5e3b2ec6 100644 --- a/src/Telemetry/src/LogTracerFactory.php +++ b/src/Telemetry/src/LogTracerFactory.php @@ -21,7 +21,7 @@ final class LogTracerFactory implements TracerFactoryInterface private readonly LoggerInterface $logger; public function __construct( - #[Proxy] private readonly ScopeInterface $scope, + private readonly ScopeInterface $scope, private readonly ClockInterface $clock, LogsInterface $logs, string $channel = self::LOG_CHANNEL diff --git a/src/Telemetry/src/NullTracerFactory.php b/src/Telemetry/src/NullTracerFactory.php index ee2f5f88a..1f0fd416e 100644 --- a/src/Telemetry/src/NullTracerFactory.php +++ b/src/Telemetry/src/NullTracerFactory.php @@ -15,7 +15,7 @@ final class NullTracerFactory implements TracerFactoryInterface { public function __construct( - #[Proxy] private readonly ?ScopeInterface $scope = new Container(), + private readonly ?ScopeInterface $scope = new Container(), ) { } diff --git a/src/Telemetry/tests/LogTracerFactoryTest.php b/src/Telemetry/tests/LogTracerFactoryTest.php index eb5a0a543..70524199f 100644 --- a/src/Telemetry/tests/LogTracerFactoryTest.php +++ b/src/Telemetry/tests/LogTracerFactoryTest.php @@ -4,7 +4,7 @@ namespace Spiral\Tests\Telemetry; -use Mockery as m; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Spiral\Core\ScopeInterface; @@ -15,26 +15,24 @@ final class LogTracerFactoryTest extends TestCase { - use m\Adapter\Phpunit\MockeryPHPUnitIntegration; - public function testMake(): void { - $logs = m::mock(LogsInterface::class); + $logs = $this->createMock(LogsInterface::class); - $logs->shouldReceive('getLogger')->once() + $logs->expects($this->once()) + ->method('getLogger') ->with('some-channel') - ->andReturn($logger = m::mock(LoggerInterface::class)); + ->willReturn($logger = $this->createMock(LoggerInterface::class)); $factory = new LogTracerFactory( - $scope = m::mock(ScopeInterface::class), - $clock = m::mock(ClockInterface::class), + $scope = $this->createMock(ScopeInterface::class), + $clock = $this->createMock(ClockInterface::class), $logs, 'some-channel' ); - $clock->shouldReceive('now'); - $scope->shouldReceive('runScope')->once(); - $logger->shouldReceive('debug')->once(); + $clock->expects($this->any())->method('now'); + $logger->expects($this->once())->method('debug'); $this->assertInstanceOf(LogTracer::class, $tracer = $factory->make()); diff --git a/src/Telemetry/tests/Monolog/TelemetryProcessorTest.php b/src/Telemetry/tests/Monolog/TelemetryProcessorTest.php index 55bb471ae..30aeb7ebf 100644 --- a/src/Telemetry/tests/Monolog/TelemetryProcessorTest.php +++ b/src/Telemetry/tests/Monolog/TelemetryProcessorTest.php @@ -5,11 +5,13 @@ namespace Spiral\Tests\Telemetry\Monolog; use Mockery as m; +use PHPUnit\Framework\Attributes\RunClassInSeparateProcess; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use Spiral\Telemetry\Monolog\TelemetryProcessor; use Spiral\Telemetry\TracerInterface; +#[RunClassInSeparateProcess] final class TelemetryProcessorTest extends TestCase { use m\Adapter\Phpunit\MockeryPHPUnitIntegration; diff --git a/src/Telemetry/tests/NullTracerFactoryTest.php b/src/Telemetry/tests/NullTracerFactoryTest.php index 178821076..ca2e7ba36 100644 --- a/src/Telemetry/tests/NullTracerFactoryTest.php +++ b/src/Telemetry/tests/NullTracerFactoryTest.php @@ -5,6 +5,7 @@ namespace Spiral\Tests\Telemetry; use Mockery as m; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; use Spiral\Core\ScopeInterface; use Spiral\Telemetry\NullTracer; @@ -14,16 +15,11 @@ final class NullTracerFactoryTest extends TestCase { use m\Adapter\Phpunit\MockeryPHPUnitIntegration; + #[RunInSeparateProcess] public function testMake(): void { - $factory = new NullTracerFactory( - $scope = m::mock(ScopeInterface::class) - ); + $factory = new NullTracerFactory(m::mock(ScopeInterface::class)); - $scope->shouldReceive('runScope')->once(); - - $this->assertInstanceOf(NullTracer::class, $tracer = $factory->make()); - - $tracer->trace('foo', fn() => 'hello'); + $this->assertInstanceOf(NullTracer::class, $factory->make()); } } diff --git a/src/Telemetry/tests/NullTracerTest.php b/src/Telemetry/tests/NullTracerTest.php index 839470a49..d184b9529 100644 --- a/src/Telemetry/tests/NullTracerTest.php +++ b/src/Telemetry/tests/NullTracerTest.php @@ -5,7 +5,11 @@ namespace Spiral\Tests\Telemetry; use Mockery as m; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Spiral\Core\BinderInterface; +use Spiral\Core\ContainerScope; use Spiral\Core\InvokerInterface; use Spiral\Core\ScopeInterface; use Spiral\Telemetry\NullTracer; @@ -16,7 +20,8 @@ final class NullTracerTest extends TestCase { use m\Adapter\Phpunit\MockeryPHPUnitIntegration; - public function testTrace(): void + #[RunInSeparateProcess] + public function testFallbackRunScope(): void { $tracer = new NullTracer( $scope = m::mock(ScopeInterface::class) @@ -43,4 +48,41 @@ public function testTrace(): void $tracer->trace('foo', $callable, ['foo' => 'bar']) ); } + + #[RunInSeparateProcess] + public function testWithScopedContainer(): void + { + $tracer = new NullTracer( + $scope = m::mock(ScopeInterface::class) + ); + + $invoker = m::mock(InvokerInterface::class); + $binder = m::mock(BinderInterface::class); + $container = m::mock(ContainerInterface::class); + $container->expects('get') + ->with(InvokerInterface::class) + ->andReturn($invoker); + $container->expects('get') + ->with(BinderInterface::class) + ->andReturn($binder); + + $callable = fn() => 'hello'; + + $invoker->shouldReceive('invoke') + ->once() + ->with($callable) + ->andReturn('hello'); + $binder->shouldReceive('bindSingleton') + ->once(); + $binder->shouldReceive('removeBinding') + ->with(SpanInterface::class); + $scope->shouldNotReceive('runScope'); + + ContainerScope::runScope($container, function () use ($tracer, $callable) { + $this->assertSame( + 'hello', + $tracer->trace('foo', $callable, ['foo' => 'bar']) + ); + }); + } }