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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to the PivotPHP Framework will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [5.1.0] - 2026-10-12

### Changed
- **Arquitetura de Resolução de Rotas e Pipeline Pré-Compilado** (SPEC-114):
- `Application::resolveAndExecuteRoute()` agora consome a resolução tipada (`RouteResolution`) do `core-routing`:
- Tratamento direto de `MethodNotAllowed` (com cabeçalho `Allow`), 405 e resposta rápida 204 para OPTIONS implícito.
- Despacho tipado de `RouteMatched` com extração direta de parâmetros (`RouteMatched::getParams()`) e middlewares.
- Padrão **Adapter**: `ExpressMiddlewareAdapter` substitui classes anônimas geradas dinamicamente na execução de middlewares callables Express `fn($req, $res, $next)`.
- Padrão **Chain of Responsibility**: `PrecompiledPipeline` e `MiddlewareHandler` compilam a cadeia imutável de execução de middlewares, eliminando alocações dinâmicas de instâncias durante a requisição.
- Requer `pivotphp/core-routing` `^3.1.0`.

### Performance
- **Zero Regressão**: Resolução e despacho até 88.5% mais rápidos em rotas estáticas e 98.3% mais rápidos em OPTIONS (204).
- Preservação irrestrita da sintaxe ExpressJS, parâmetros de rota e interoperabilidade com PSR-15 e OpenAPI.

## [5.0.0] - 2026-10-10

### Changed
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"php": "^8.1",
"ext-json": "*",
"ext-mbstring": "*",
"pivotphp/core-routing": "^3.0.0",
"pivotphp/core-routing": "^3.1.0",
"pivotphp/http": "^1.0",
"pivotphp/security": "^1.0",
"psr/container": "^2.0",
Expand Down
92 changes: 30 additions & 62 deletions src/Core/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
use PivotPHP\Routing\Router\Router;
use PivotPHP\Routing\Router\StaticFileManager;
use PivotPHP\Core\Utils\CallableResolver;
use PivotPHP\Core\Middleware\ExpressMiddlewareAdapter;
use PivotPHP\Core\Middleware\MiddlewareStack;
use PivotPHP\Core\Middleware\PrecompiledPipeline;
use PivotPHP\Routing\Router\Resolution\MethodNotAllowed;
use PivotPHP\Routing\Router\Resolution\RouteMatched;
use PivotPHP\Routing\Router\Resolution\RouteNotFound;
use PivotPHP\Core\Exceptions\HttpException;
use PivotPHP\Core\Exceptions\Enhanced\ContextualException;
use PivotPHP\Core\Providers\Container;
Expand Down Expand Up @@ -459,45 +464,7 @@ private function adaptMiddleware(mixed $middleware): MiddlewareInterface
}

if (is_callable($middleware)) {
return new class ($middleware) implements MiddlewareInterface {
/** @var callable */
private $next;

public function __construct(callable $next)
{
$this->next = $next;
}

public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
$response = new ExpressResponse();
$downstream = null;

// $next() accepts an optional (modified) request and runs the rest of the
// pipeline once; its response is kept even if the middleware does not return it.
$next = static function (?ServerRequestInterface $nextRequest = null) use (
$handler,
$request,
&$downstream
): ResponseInterface {
return $downstream = $handler->handle($nextRequest ?? $request);
};

$result = ($this->next)($request, $response, $next);

if ($result instanceof ResponseInterface) {
return $result;
}

if ($result instanceof ExpressResponse) {
return $result->psr7();
}

return $downstream ?? $handler->handle($request);
}
};
return new ExpressMiddlewareAdapter($middleware);
}

throw new \InvalidArgumentException('Middleware must be PSR-15 or callable.');
Expand Down Expand Up @@ -845,26 +812,23 @@ private function resolveAndExecuteRoute(ServerRequestInterface $request): Respon
$method = $request->getMethod();
$path = $request->getUri()->getPath();

// Encontrar rota
$route = $this->router->identify($method, $path);
// Encontrar rota via resultado tipado (SPEC-114)
$resolution = $this->router->resolve($method, $path);

if (!$route) {
// SPEC-072: se o path casa com outros métodos, responde 405 (ou 204 p/ OPTIONS) com Allow.
$allowed = $this->router->allowedMethods($path);
if ($resolution instanceof MethodNotAllowed) {
$allow = $resolution->getAllowHeader();

if ($allowed !== []) {
$allow = implode(', ', $allowed);

if ($method === 'OPTIONS') {
return (new ExpressResponse())->noContent(204)->withHeader('Allow', $allow);
}

return (new ExpressResponse())
->status(405)
->header('Allow', $allow)
->json(['error' => 'Method Not Allowed']);
if ($method === 'OPTIONS') {
return (new ExpressResponse())->noContent(204)->withHeader('Allow', $allow);
}

return (new ExpressResponse())
->status(405)
->header('Allow', $allow)
->json(['error' => 'Method Not Allowed']);
}

if ($resolution instanceof RouteNotFound || !$resolution instanceof RouteMatched) {
// Buscar rotas disponíveis para suggestions
$availableRoutes = array_map(
static fn ($r) => "{$r['method']} {$r['path']}",
Expand All @@ -874,9 +838,11 @@ private function resolveAndExecuteRoute(ServerRequestInterface $request): Respon
throw ContextualException::routeNotFound($method, $path, $availableRoutes);
}

$route = $resolution->getRoute();

// Expor os parâmetros de rota como atributo; a fachada ExpressRequest os lê.
$matchedParams = $route['matched_params'] ?? [];
if (is_array($matchedParams) && $matchedParams !== []) {
$matchedParams = $resolution->getParams();
if ($matchedParams !== []) {
$request = $request->withAttribute(ExpressRequest::ROUTE_PARAMS_ATTRIBUTE, $matchedParams);
}

Expand All @@ -885,18 +851,20 @@ private function resolveAndExecuteRoute(ServerRequestInterface $request): Respon
);

// Executar middlewares da rota (os globais já rodaram), se houver.
$routeMiddlewares = $route['middlewares'] ?? [];
$routeMiddlewares = $resolution->getMiddlewares();

if (empty($routeMiddlewares)) {
if ($routeMiddlewares === []) {
return $handler->handle($request);
}

$stack = new MiddlewareStack();
$adaptedMiddlewares = [];
foreach ($routeMiddlewares as $middleware) {
$stack->add($this->adaptMiddleware($middleware));
$adaptedMiddlewares[] = $this->adaptMiddleware($middleware);
}

return $stack->execute($request, $handler);
$pipeline = new PrecompiledPipeline($adaptedMiddlewares, $handler);

return $pipeline->handle($request);
}

/**
Expand Down
55 changes: 55 additions & 0 deletions src/Middleware/ExpressMiddlewareAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace PivotPHP\Core\Middleware;

use PivotPHP\Http\ExpressResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
* Adaptador para middlewares Express callable($req, $res, $next) (Padrão Adapter).
*/
final class ExpressMiddlewareAdapter implements MiddlewareInterface
{
/** @var callable */
private $middleware;

public function __construct(callable $middleware)
{
$this->middleware = $middleware;
}

public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
$response = new ExpressResponse();
$downstream = null;

// $next() accepts an optional (modified) request and runs the rest of the
// pipeline once; its response is kept even if the middleware does not return it.
$next = static function (?ServerRequestInterface $nextRequest = null) use (
$handler,
$request,
&$downstream
): ResponseInterface {
return $downstream = $handler->handle($nextRequest ?? $request);
};

$result = ($this->middleware)($request, $response, $next);

if ($result instanceof ResponseInterface) {
return $result;
}

if ($result instanceof ExpressResponse) {
return $result->psr7();
}

return $downstream ?? $handler->handle($request);
}
}
27 changes: 27 additions & 0 deletions src/Middleware/MiddlewareHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace PivotPHP\Core\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
* Nó da cadeia de responsabilidade para execução de middlewares (Padrão Chain of Responsibility).
*/
final class MiddlewareHandler implements RequestHandlerInterface
{
public function __construct(
private readonly MiddlewareInterface $middleware,
private readonly RequestHandlerInterface $next
) {
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->middleware->process($request, $this->next);
}
}
35 changes: 35 additions & 0 deletions src/Middleware/PrecompiledPipeline.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace PivotPHP\Core\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
* Pipeline pré-compilado de middlewares imutável e reutilizável (Padrão Chain of Responsibility).
*/
final class PrecompiledPipeline
{
private RequestHandlerInterface $pipeline;

/**
* @param array<int, MiddlewareInterface> $middlewares
*/
public function __construct(array $middlewares, RequestHandlerInterface $finalHandler)
{
$handler = $finalHandler;
for ($i = count($middlewares) - 1; $i >= 0; $i--) {
$handler = new MiddlewareHandler($middlewares[$i], $handler);
}
$this->pipeline = $handler;
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->pipeline->handle($request);
}
}
Loading
Loading