diff --git a/src/CurrentRequest.php b/src/CurrentRequest.php index 4feee69c..244c5e00 100644 --- a/src/CurrentRequest.php +++ b/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.php b/src/Http.php index 37442877..6105904a 100644 --- a/src/Http.php +++ b/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/LazyPipeline.php b/src/LazyPipeline.php new file mode 100644 index 00000000..b2576c40 --- /dev/null +++ b/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/Pipeline.php b/src/Pipeline.php index 79dc91ae..38259e09 100644 --- a/src/Pipeline.php +++ b/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/tests/HttpTest.php b/tests/HttpTest.php index 72a17b63..90b52ac0 100644 --- a/tests/HttpTest.php +++ b/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)