Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 63 additions & 50 deletions src/DispatchContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

use function in_array;
use function is_a;
use function preg_quote;
use function preg_replace;
use function rawurldecode;
use function rtrim;
use function set_error_handler;
Expand All @@ -29,14 +31,12 @@
final class DispatchContext implements ContainerInterface
{
/** @var array<int, mixed> */
public array $params = [];
public private(set) array $params = [];

public AbstractRoute|null $route = null;
public private(set) AbstractRoute|null $route = null;

/** @var array<string, string> Headers to apply only when the response does not already have them */
public array $defaultResponseHeaders = [];

private RoutinePipeline|null $routinePipeline = null;
public private(set) array $defaultResponseHeaders = [];

private Responder|null $responder = null;

Expand All @@ -49,21 +49,32 @@ final class DispatchContext implements ContainerInterface

private bool $hasStatusOverride = false;

private string $effectiveMethod = '';

private string $effectivePath = '';
private string $effectiveMethod;

/** @var array<int, AbstractRoute> */
private array $handlers = [];
private string $effectivePath;

private Resolver|null $resolver = null;

/** @param array<int, AbstractRoute> $handlers */
public function __construct(
public ServerRequestInterface $request,
public ResponseFactoryInterface&StreamFactoryInterface $factory,
public private(set) ServerRequestInterface $request,
public private(set) ResponseFactoryInterface&StreamFactoryInterface $factory,
private RoutinePipeline $routinePipeline = new RoutinePipeline(),
private array $handlers = [],
string $basePath = '',
) {
$this->effectivePath = rtrim(rawurldecode($request->getUri()->getPath()), ' /');
$path = rtrim(rawurldecode($request->getUri()->getPath()), ' /');
if ($basePath !== '') {
$path = preg_replace(
'#^' . preg_quote($basePath, '#') . '#',
'',
$path,
) ?? $path;
}

$this->effectivePath = $path;
$this->effectiveMethod = strtoupper($request->getMethod());
$this->resetHandlerState();
}

public function method(): string
Expand All @@ -76,11 +87,6 @@ public function path(): string
return $this->effectivePath;
}

public function setPath(string $path): void
{
$this->effectivePath = $path;
}

public function hasPreparedResponse(): bool
{
return $this->hasPreparedResponse;
Expand Down Expand Up @@ -125,6 +131,13 @@ public function prepareResponse(int $status, array $headers = []): void
}
}

/** @param array<int, mixed> $params */
public function configureRoute(AbstractRoute $route, array $params = []): void
{
$this->route = $route;
$this->params = $params;
}

/** Generates the PSR-7 response from the current route */
public function response(): ResponseInterface|null
{
Expand All @@ -146,7 +159,7 @@ public function response(): ResponseInterface|null
$previousErrorHandler = $isHandler ? null : $this->installErrorHandler();

try {
$preRoutineResult = $this->routinePipeline()->processBy($this, $route);
$preRoutineResult = $this->routinePipeline->processBy($this, $route);

if ($preRoutineResult instanceof AbstractRoute) {
return $this->forward($preRoutineResult);
Expand All @@ -166,7 +179,7 @@ public function response(): ResponseInterface|null
return $this->forward($rawResult);
}

$processedResult = $this->routinePipeline()->processThrough($this, $route, $rawResult);
$processedResult = $this->routinePipeline->processThrough($this, $route, $rawResult);

if (!$isHandler) {
$errorResponse = $this->forwardCollectedErrors();
Expand All @@ -192,27 +205,21 @@ public function response(): ResponseInterface|null
}
}

public function forward(AbstractRoute $route): ResponseInterface|null
public function withRequest(ServerRequestInterface $request): void
{
$this->route = $route;

return $this->response();
$this->request = $request;
}

public function setRoutinePipeline(RoutinePipeline $routinePipeline): void
public function setResponder(Responder $responder): void
{
$this->routinePipeline = $routinePipeline;
$this->responder = $responder;
}

/** @param array<int, AbstractRoute> $handlers */
public function setHandlers(array $handlers): void
public function forward(AbstractRoute $route): ResponseInterface|null
{
$this->handlers = $handlers;
}
$this->route = $route;

public function setResponder(Responder $responder): void
{
$this->responder = $responder;
return $this->response();
}

public function resolver(): Resolver
Expand All @@ -239,7 +246,26 @@ public function get(string $id): mixed
throw new NotFoundException(sprintf('No entry found for "%s"', $id));
}

/** @return callable|null The previous error handler, or null if no ErrorHandler is registered */
private function resetHandlerState(): void
{
foreach ($this->handlers as $handler) {
if ($handler instanceof ErrorHandler) {
$handler->errors = [];
} elseif ($handler instanceof ExceptionHandler) {
$handler->exception = null;
}
}
}

/**
* Safe only when requests are guaranteed not to overlap within the same PHP process
* (for example: PHP-FPM, Swoole workers, FrankenPHP workers, or ReactPHP when
* request handling/dispatch is strictly serialized). Not safe for coroutine- or
* event-loop-concurrent request handling within a single PHP process, since
* set_error_handler is global state.
*
* @return callable|null The previous error handler, or null if no ErrorHandler is registered
*/
private function installErrorHandler(): callable|null
{
foreach ($this->handlers as $handler) {
Expand Down Expand Up @@ -303,7 +329,7 @@ private function forwardToStatusRoute(ResponseInterface $preparedResponse): Resp

// Run routine negotiation (e.g. Accept) before forwarding,
// since the normal route-selection phase was skipped
$this->routinePipeline()->matches($this, $handler, $this->params);
$this->routinePipeline->matches($this, $handler, $this->params);

$result = $this->forward($handler);

Expand All @@ -327,27 +353,14 @@ private function finalizeResponse(mixed $response): ResponseInterface
);
}

private function routinePipeline(): RoutinePipeline
{
return $this->routinePipeline ??= new RoutinePipeline();
}

private function responder(): Responder
{
if ($this->responder !== null) {
return $this->responder;
}

return $this->responder = new Responder($this->factory);
return $this->responder ??= new Responder($this->factory);
}

private function ensureResponseDraft(): ResponseInterface
{
if ($this->responseDraft !== null) {
return $this->responseDraft;
}

return $this->responseDraft = $this->factory->createResponse();
return $this->responseDraft ??= $this->factory->createResponse();
}

public function __toString(): string
Expand Down
59 changes: 10 additions & 49 deletions src/DispatchEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Respect\Rest;

use Closure;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
Expand All @@ -19,28 +18,32 @@
use function count;
use function implode;
use function iterator_to_array;
use function preg_quote;
use function preg_replace;
use function stripos;

final class DispatchEngine implements RequestHandlerInterface
{
private RoutinePipeline $routinePipeline;

/** @param (Closure(DispatchContext): void)|null $onContextReady */
public function __construct(
private RouteProvider $routeProvider,
private ResponseFactoryInterface&StreamFactoryInterface $factory,
private Closure|null $onContextReady = null,
) {
$this->routinePipeline = new RoutinePipeline();
}

public function routinePipeline(): RoutinePipeline
{
return $this->routinePipeline;
}

public function dispatch(ServerRequestInterface $serverRequest): DispatchContext
{
$context = new DispatchContext(
$serverRequest,
$this->factory,
$this->routinePipeline,
$this->routeProvider->getHandlers(),
$this->routeProvider->getBasePath(),
);

return $this->dispatchContext($context);
Expand All @@ -55,13 +58,6 @@ public function handle(ServerRequestInterface $request): ResponseInterface

public function dispatchContext(DispatchContext $context): DispatchContext
{
if ($this->onContextReady !== null) {
($this->onContextReady)($context);
}

$context->setRoutinePipeline($this->routinePipeline);
$context->setHandlers($this->routeProvider->getHandlers());

if (!$this->isRoutelessDispatch($context) && $context->route === null) {
$this->routeDispatch($context);
}
Expand Down Expand Up @@ -120,8 +116,6 @@ private function isRoutelessDispatch(DispatchContext $context): bool

private function routeDispatch(DispatchContext $context): void
{
$this->applyBasePath($context);

$matchedByPath = $this->getMatchedRoutesByPath($context);
/** @var array<int, AbstractRoute> $matchedArray */
$matchedArray = iterator_to_array($matchedByPath);
Expand All @@ -140,30 +134,13 @@ private function routeDispatch(DispatchContext $context): void
}
}

private function applyBasePath(DispatchContext $context): void
{
$basePath = $this->routeProvider->getBasePath();
if ($basePath === '') {
return;
}

$context->setPath(
preg_replace(
'#^' . preg_quote($basePath) . '#',
'',
$context->path(),
) ?? $context->path(),
);
}

/** @param array<int, mixed> $params */
private function configureContext(
DispatchContext $context,
AbstractRoute $route,
array $params = [],
): DispatchContext {
$context->route = $route;
$context->params = $params;
$context->configureRoute($route, $params);

return $context;
}
Expand All @@ -176,7 +153,7 @@ private function getMatchedRoutesByPath(DispatchContext $context): SplObjectStor

foreach ($this->routeProvider->getRoutes() as $route) {
$params = [];
if (!$this->matchRoute($context, $route, $params)) {
if (!$route->match($context, $params)) {
continue;
}

Expand Down Expand Up @@ -265,21 +242,6 @@ private function hasExplicitOptionsRoute(SplObjectStorage $matchedByPath): bool
return false;
}

/** @param array<int, mixed> $params */
private function matchRoute(
DispatchContext $context,
AbstractRoute $route,
array &$params = [],
): bool {
if (!$route->match($context, $params)) {
return false;
}

$context->route = $route;

return true;
}

/** @param SplObjectStorage<AbstractRoute, array<int, mixed>> $matchedByPath */
private function routineMatch(
DispatchContext $context,
Expand All @@ -296,7 +258,6 @@ private function routineMatch(
/** @var array<int, mixed> $tempParams */
$tempParams = $matchedByPath[$route];
$context->clearResponseMeta();
$context->route = $route;
if ($this->routinePipeline->matches($context, $route, $tempParams)) {
return $this->configureContext(
$context,
Expand Down
Loading