Skip to content

Commit

Permalink
Merge pull request #1168: add LazyPipeline, improve Telemetry
Browse files Browse the repository at this point in the history
  • Loading branch information
roxblnfk authored Nov 29, 2024
2 parents c32dfed + d730539 commit fc1ed08
Show file tree
Hide file tree
Showing 33 changed files with 470 additions and 105 deletions.
4 changes: 2 additions & 2 deletions src/Framework/Bootloader/Http/HttpBootloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down
16 changes: 10 additions & 6 deletions src/Framework/Bootloader/Http/RoutesBootloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
);
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/Http/src/CurrentRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
Expand Down
24 changes: 19 additions & 5 deletions src/Http/src/Http.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down
149 changes: 149 additions & 0 deletions src/Http/src/LazyPipeline.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

declare(strict_types=1);

namespace Spiral\Http;

use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Spiral\Core\Attribute\Proxy;
use Spiral\Core\Container\Autowire;
use Spiral\Http\Event\MiddlewareProcessing;
use Spiral\Http\Exception\PipelineException;
use Spiral\Telemetry\SpanInterface;
use Spiral\Telemetry\TracerInterface;

/**
* Pipeline used to pass request and response thought the chain of middleware.
* This kind of pipeline creates middleware on the fly.
*/
final class LazyPipeline implements RequestHandlerInterface, MiddlewareInterface
{
/**
* Set of middleware to be applied for every request.
*
* @var list<MiddlewareInterface|Autowire|string>
*/
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);
}
}
3 changes: 2 additions & 1 deletion src/Http/src/Pipeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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
) {
Expand Down
4 changes: 2 additions & 2 deletions src/Http/tests/HttpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions src/Router/src/CoreHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -124,6 +125,9 @@ public function handle(Request $request): Response
new CallContext(
Target::fromPair($controller, $action),
$parameters,
[
ServerRequestInterface::class => $request,
],
),
),
attributes: [
Expand Down
9 changes: 5 additions & 4 deletions src/Router/src/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit fc1ed08

Please sign in to comment.