From 91efc5ffb90b02e6aea1c3709eab1d4fac4820e4 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 6 Sep 2025 15:09:26 +0200 Subject: [PATCH 01/12] simplify Application::route() --- CHANGELOG.md | 3 ++ composer.json | 2 +- src/Application.php | 12 ++++--- src/Application/Async/Http.php | 35 ++------------------- src/Application/Cli.php | 2 +- src/Application/Http.php | 35 ++------------------- src/Application/Implementation.php | 10 +++--- src/Http/To.php | 50 ------------------------------ tests/ApplicationTest.php | 36 ++++++++++++++------- 9 files changed, 49 insertions(+), 136 deletions(-) delete mode 100644 src/Http/To.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fe1e45..7b5dcca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,14 @@ - Requires `innmind/foundation:~1.9` - Requires `innmind/di:~3.0` - `Innmind\Framework\Application::route()` callable must now return a `Innmind\Router\Component` +- `Innmind\Framework\Application::route()` callable first parameter now is a `Innmint\Router\Pipe` +- `Innmind\Framework\Application::route()` first parameter must now be expressed via a component inside the callable ### Removed - The ability to use `string`s to reference services - `Innmind\Framework\Http\Service` +- `Innmind\Framework\Http\To` ### Fixed diff --git a/composer.json b/composer.json index 41de291..a63cc87 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "php": "~8.2", "innmind/foundation": "~1.9", "innmind/di": "~3.0", - "innmind/router": "^5.0.1" + "innmind/router": "~5.1" }, "autoload": { "psr-4": { diff --git a/src/Application.php b/src/Application.php index 1bf8918..bf4c6f5 100644 --- a/src/Application.php +++ b/src/Application.php @@ -16,7 +16,10 @@ Container, Service, }; -use Innmind\Router\Component; +use Innmind\Router\{ + Component, + Pipe, +}; use Innmind\Http\{ ServerRequest, Response, @@ -147,14 +150,13 @@ public function mapCommand(callable $map): self /** * @psalm-mutation-free * - * @param literal-string $pattern - * @param callable(Container, OperatingSystem, Environment): Component $handle + * @param callable(Pipe, Container, OperatingSystem, Environment): Component $handle * * @return self */ - public function route(string $pattern, callable $handle): self + public function route(callable $handle): self { - return new self($this->app->route($pattern, $handle)); + return new self($this->app->route($handle)); } /** diff --git a/src/Application/Async/Http.php b/src/Application/Async/Http.php index 4d08e46..65794e3 100644 --- a/src/Application/Async/Http.php +++ b/src/Application/Async/Http.php @@ -21,10 +21,7 @@ Builder, Service, }; -use Innmind\Router\{ - Method, - Endpoint, -}; +use Innmind\Router\Pipe; use Innmind\Http\{ ServerRequest, Response, @@ -168,37 +165,11 @@ public function mapCommand(callable $map): self * @psalm-mutation-free */ #[\Override] - public function route(string $pattern, callable $handle): self + public function route(callable $handle): self { - /** - * @psalm-suppress PossiblyUndefinedArrayOffset Todo better typing - * @var literal-string $path - */ - [$method, $path] = \explode(' ', $pattern, 2); - - $method = match ($method) { - 'get', 'GET' => Method::get(), - 'post', 'POST' => Method::post(), - 'put', 'PUT' => Method::put(), - 'patch', 'PATCH' => Method::patch(), - 'delete', 'DELETE' => Method::delete(), - 'options', 'OPTIONS' => Method::options(), - 'trace', 'TRACE' => Method::trace(), - 'connect', 'CONNECT' => Method::connect(), - 'head', 'HEAD' => Method::head(), - 'link', 'LINK' => Method::link(), - 'unlink', 'UNLINK' => Method::unlink(), - }; - - /** - * @psalm-suppress MixedArgumentTypeCoercion - * @psalm-suppress InvalidArgument - */ return $this->appendRoutes( static fn($routes, $container, $os, $env) => $routes->add( - $method - ->pipe(Endpoint::of($path)) - ->pipe($handle($container, $os, $env)), + $handle(Pipe::new(), $container, $os, $env), ), ); } diff --git a/src/Application/Cli.php b/src/Application/Cli.php index 63574a2..02c765b 100644 --- a/src/Application/Cli.php +++ b/src/Application/Cli.php @@ -158,7 +158,7 @@ public function mapCommand(callable $map): self * @psalm-mutation-free */ #[\Override] - public function route(string $pattern, callable $handle): self + public function route(callable $handle): self { return $this; } diff --git a/src/Application/Http.php b/src/Application/Http.php index 656cb94..0f900cb 100644 --- a/src/Application/Http.php +++ b/src/Application/Http.php @@ -15,10 +15,7 @@ Builder, Service, }; -use Innmind\Router\{ - Method, - Endpoint, -}; +use Innmind\Router\Pipe; use Innmind\Http\{ ServerRequest, Response, @@ -147,37 +144,11 @@ public function mapCommand(callable $map): self * @psalm-mutation-free */ #[\Override] - public function route(string $pattern, callable $handle): self + public function route(callable $handle): self { - /** - * @psalm-suppress PossiblyUndefinedArrayOffset Todo better typing - * @var literal-string $path - */ - [$method, $path] = \explode(' ', $pattern, 2); - - $method = match ($method) { - 'get', 'GET' => Method::get(), - 'post', 'POST' => Method::post(), - 'put', 'PUT' => Method::put(), - 'patch', 'PATCH' => Method::patch(), - 'delete', 'DELETE' => Method::delete(), - 'options', 'OPTIONS' => Method::options(), - 'trace', 'TRACE' => Method::trace(), - 'connect', 'CONNECT' => Method::connect(), - 'head', 'HEAD' => Method::head(), - 'link', 'LINK' => Method::link(), - 'unlink', 'UNLINK' => Method::unlink(), - }; - - /** - * @psalm-suppress MixedArgumentTypeCoercion - * @psalm-suppress InvalidArgument - */ return $this->appendRoutes( static fn($routes, $container, $os, $env) => $routes->add( - $method - ->pipe(Endpoint::of($path)) - ->pipe($handle($container, $os, $env)), + $handle(Pipe::new(), $container, $os, $env), ), ); } diff --git a/src/Application/Implementation.php b/src/Application/Implementation.php index 58644f0..a8248a5 100644 --- a/src/Application/Implementation.php +++ b/src/Application/Implementation.php @@ -17,7 +17,10 @@ Container, Service, }; -use Innmind\Router\Component; +use Innmind\Router\{ + Component, + Pipe, +}; use Innmind\Http\{ ServerRequest, Response, @@ -82,12 +85,11 @@ public function mapCommand(callable $map): self; /** * @psalm-mutation-free * - * @param literal-string $pattern - * @param callable(Container, OperatingSystem, Environment): Component $handle + * @param callable(Pipe, Container, OperatingSystem, Environment): Component $handle * * @return self */ - public function route(string $pattern, callable $handle): self; + public function route(callable $handle): self; /** * @psalm-mutation-free diff --git a/src/Http/To.php b/src/Http/To.php deleted file mode 100644 index ba1a100..0000000 --- a/src/Http/To.php +++ /dev/null @@ -1,50 +0,0 @@ -, Response> - */ - public function __invoke( - Container $container, - OperatingSystem $os, - Environment $env, - ): Component { - $service = $this->service; - - /** - * @psalm-suppress MissingClosureReturnType - * @psalm-suppress MixedArgumentTypeCoercion - * @psalm-suppress InvalidFunctionCall - * @todo fix non lazy call - */ - return Handle::of( - $container($service), - ); - } - - public static function service(Service $service): self - { - return new self($service); - } -} diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index d48067f..04d5e24 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -10,7 +10,6 @@ Middleware\Optional, Middleware\LoadDotEnv, Http\RequestHandler, - Http\To, }; use Innmind\OperatingSystem\Factory; use Innmind\CLI\{ @@ -905,16 +904,26 @@ public function testRouteShortDeclaration(): BlackBox\Proof $responseB = Response::of(StatusCode::ok, $protocol); $app = Application::http(Factory::build(), Environment::test($variables)) - ->route('GET /foo', fn() => Handle::via(function($request) use ($protocol, $responseA) { - $this->assertSame($protocol, $request->protocolVersion()); + ->route( + fn($pipe) => $pipe + ->get() + ->endpoint('/foo') + ->handle(function($request) use ($protocol, $responseA) { + $this->assertSame($protocol, $request->protocolVersion()); - return Attempt::result($responseA); - })) - ->route('GET /bar', fn() => Handle::via(function($request) use ($protocol, $responseB) { - $this->assertSame($protocol, $request->protocolVersion()); + return Attempt::result($responseA); + }), + ) + ->route( + fn($pipe) => $pipe + ->post() + ->endpoint('/bar') + ->handle(function($request) use ($protocol, $responseB) { + $this->assertSame($protocol, $request->protocolVersion()); - return Attempt::result($responseB); - })); + return Attempt::result($responseB); + }), + ); $response = $app->run(ServerRequest::of( Url::of('/foo'), @@ -926,7 +935,7 @@ public function testRouteShortDeclaration(): BlackBox\Proof $response = $app->run(ServerRequest::of( Url::of('/bar'), - Method::get, + Method::post, $protocol, )); @@ -951,7 +960,12 @@ public function testRouteToService(): BlackBox\Proof $expected = Response::of(StatusCode::ok, $protocol); $app = Application::http(Factory::build(), Environment::test($variables)) - ->route('GET /foo', To::service(Services::responseHandler)) + ->route( + static fn($pipe, $container) => $pipe + ->get() + ->endpoint('/foo') + ->handle($container(Services::responseHandler)), + ) ->service(Services::responseHandler, static fn() => new class($expected) { public function __construct(private $response) { From 3d8d4c107826bd8bd6f5acdad04ed6a82d230acf Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 6 Sep 2025 15:10:47 +0200 Subject: [PATCH 02/12] remove Application::appendRoutes() to enforce composition --- CHANGELOG.md | 2 + fixtures/server.php | 18 +++------ src/Application.php | 17 +-------- src/Application/Async/Http.php | 39 ++++++------------- src/Application/Cli.php | 9 ----- src/Application/Http.php | 39 ++++++------------- src/Application/Implementation.php | 10 ----- src/Http/Routes.php | 58 ---------------------------- tests/ApplicationTest.php | 61 +++++++++++++++--------------- 9 files changed, 64 insertions(+), 189 deletions(-) delete mode 100644 src/Http/Routes.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b5dcca..d22f9fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ - The ability to use `string`s to reference services - `Innmind\Framework\Http\Service` - `Innmind\Framework\Http\To` +- `Innmind\Framework\Http\Routes` +- `Innmind\Framework\Application::appendRoutes()` ### Fixed diff --git a/fixtures/server.php b/fixtures/server.php index d650585..5fca289 100644 --- a/fixtures/server.php +++ b/fixtures/server.php @@ -6,25 +6,19 @@ use Innmind\Framework\{ Application, Main\Async\Http, - Http\Routes, -}; -use Innmind\Router\{ - Method, - Endpoint, - Handle, - Respond, }; +use Innmind\Router\Respond; use Innmind\Http\Response\StatusCode; -use Innmind\Immutable\Attempt; new class extends Http { protected function configure(Application $app): Application { - return $app->appendRoutes(static fn($routes) => $routes->add( - Method::get() - ->pipe(Endpoint::of('/hello')) + return $app->route( + static fn($pipe) => $pipe + ->get() + ->endpoint('/hello') ->pipe(Respond::with(StatusCode::ok)), - )); + ); } }; diff --git a/src/Application.php b/src/Application.php index bf4c6f5..a996076 100644 --- a/src/Application.php +++ b/src/Application.php @@ -3,10 +3,7 @@ namespace Innmind\Framework; -use Innmind\Framework\Http\{ - Routes, - RequestHandler, -}; +use Innmind\Framework\Http\RequestHandler; use Innmind\OperatingSystem\OperatingSystem; use Innmind\CLI\{ Environment as CliEnv, @@ -159,18 +156,6 @@ public function route(callable $handle): self return new self($this->app->route($handle)); } - /** - * @psalm-mutation-free - * - * @param callable(Routes, Container, OperatingSystem, Environment): Routes $append - * - * @return self - */ - public function appendRoutes(callable $append): self - { - return new self($this->app->appendRoutes($append)); - } - /** * @psalm-mutation-free * diff --git a/src/Application/Async/Http.php b/src/Application/Async/Http.php index 65794e3..d7f3c5c 100644 --- a/src/Application/Async/Http.php +++ b/src/Application/Async/Http.php @@ -6,7 +6,6 @@ use Innmind\Framework\{ Environment, Application\Implementation, - Http\Routes, Http\Router, Http\RequestHandler, }; @@ -21,7 +20,10 @@ Builder, Service, }; -use Innmind\Router\Pipe; +use Innmind\Router\{ + Component, + Pipe, +}; use Innmind\Http\{ ServerRequest, Response, @@ -30,6 +32,7 @@ Maybe, Sequence, Attempt, + SideEffect, }; /** @@ -44,7 +47,7 @@ final class Http implements Implementation * * @param \Closure(OperatingSystem, Environment): array{OperatingSystem, Environment} $map * @param \Closure(OperatingSystem, Environment): Builder $container - * @param Sequence $routes + * @param Sequence> $routes * @param \Closure(RequestHandler, Container, OperatingSystem, Environment): RequestHandler $mapRequestHandler * @param Maybe $notFound */ @@ -70,7 +73,7 @@ public static function of(OperatingSystem $os): self $os, static fn(OperatingSystem $os, Environment $env) => [$os, $env], static fn() => Builder::new(), - Sequence::of(), + Sequence::lazyStartingWith(), static fn(RequestHandler $handler) => $handler, $notFound, ); @@ -166,25 +169,12 @@ public function mapCommand(callable $map): self */ #[\Override] public function route(callable $handle): self - { - return $this->appendRoutes( - static fn($routes, $container, $os, $env) => $routes->add( - $handle(Pipe::new(), $container, $os, $env), - ), - ); - } - - /** - * @psalm-mutation-free - */ - #[\Override] - public function appendRoutes(callable $append): self { return new self( $this->os, $this->map, $this->container, - ($this->routes)($append), + ($this->routes)($handle), $this->mapRequestHandler, $this->notFound, ); @@ -255,15 +245,10 @@ static function(ServerRequest $request, OperatingSystem $os) use ( $env = Environment::http($request->environment()); [$os, $env] = $map($os, $env); $container = $container($os, $env)->build(); - $routes = Sequence::lazyStartingWith($routes) - ->flatMap(static fn($routes) => $routes) - ->map(static fn($provide) => $provide( - Routes::lazy(), - $container, - $os, - $env, - )) - ->flatMap(static fn($routes) => $routes->toSequence()); + $pipe = Pipe::new(); + $routes = $routes->map( + static fn($handle) => $handle($pipe, $container, $os, $env), + ); $router = new Router( $routes, $notFound->map( diff --git a/src/Application/Cli.php b/src/Application/Cli.php index 02c765b..2293e40 100644 --- a/src/Application/Cli.php +++ b/src/Application/Cli.php @@ -163,15 +163,6 @@ public function route(callable $handle): self return $this; } - /** - * @psalm-mutation-free - */ - #[\Override] - public function appendRoutes(callable $append): self - { - return $this; - } - /** * @psalm-mutation-free */ diff --git a/src/Application/Http.php b/src/Application/Http.php index 0f900cb..bd15c57 100644 --- a/src/Application/Http.php +++ b/src/Application/Http.php @@ -5,7 +5,6 @@ use Innmind\Framework\{ Environment, - Http\Routes, Http\Router, Http\RequestHandler, }; @@ -15,7 +14,10 @@ Builder, Service, }; -use Innmind\Router\Pipe; +use Innmind\Router\{ + Component, + Pipe, +}; use Innmind\Http\{ ServerRequest, Response, @@ -23,6 +25,7 @@ use Innmind\Immutable\{ Maybe, Sequence, + SideEffect, }; /** @@ -35,7 +38,7 @@ final class Http implements Implementation * @psalm-mutation-free * * @param \Closure(OperatingSystem, Environment): Builder $container - * @param Sequence $routes + * @param Sequence> $routes * @param \Closure(RequestHandler, Container, OperatingSystem, Environment): RequestHandler $mapRequestHandler * @param Maybe $notFound */ @@ -61,7 +64,7 @@ public static function of(OperatingSystem $os, Environment $env): self $os, $env, static fn() => Builder::new(), - Sequence::of(), + Sequence::lazyStartingWith(), static fn(RequestHandler $handler) => $handler, $notFound, ); @@ -145,25 +148,12 @@ public function mapCommand(callable $map): self */ #[\Override] public function route(callable $handle): self - { - return $this->appendRoutes( - static fn($routes, $container, $os, $env) => $routes->add( - $handle(Pipe::new(), $container, $os, $env), - ), - ); - } - - /** - * @psalm-mutation-free - */ - #[\Override] - public function appendRoutes(callable $append): self { return new self( $this->os, $this->env, $this->container, - ($this->routes)($append), + ($this->routes)($handle), $this->mapRequestHandler, $this->notFound, ); @@ -219,15 +209,10 @@ public function run($input) $container = ($this->container)($this->os, $this->env)->build(); $os = $this->os; $env = $this->env; - $routes = Sequence::lazyStartingWith($this->routes) - ->flatMap(static fn($routes) => $routes) - ->map(static fn($provide) => $provide( - Routes::lazy(), - $container, - $os, - $env, - )) - ->flatMap(static fn($routes) => $routes->toSequence()); + $pipe = Pipe::new(); + $routes = $this->routes->map( + static fn($handle) => $handle($pipe, $container, $os, $env), + ); $router = new Router( $routes, $this->notFound->map( diff --git a/src/Application/Implementation.php b/src/Application/Implementation.php index a8248a5..e895a00 100644 --- a/src/Application/Implementation.php +++ b/src/Application/Implementation.php @@ -5,7 +5,6 @@ use Innmind\Framework\{ Environment, - Http\Routes, Http\RequestHandler, }; use Innmind\OperatingSystem\OperatingSystem; @@ -91,15 +90,6 @@ public function mapCommand(callable $map): self; */ public function route(callable $handle): self; - /** - * @psalm-mutation-free - * - * @param callable(Routes, Container, OperatingSystem, Environment): Routes $append - * - * @return self - */ - public function appendRoutes(callable $append): self; - /** * @psalm-mutation-free * diff --git a/src/Http/Routes.php b/src/Http/Routes.php deleted file mode 100644 index abb6a94..0000000 --- a/src/Http/Routes.php +++ /dev/null @@ -1,58 +0,0 @@ -> $routes - */ - private function __construct(private Sequence $routes) - { - } - - /** - * @psalm-pure - */ - public static function lazy(): self - { - return new self(Sequence::lazyStartingWith()); - } - - /** - * @param Component $route - */ - public function add(Component $route): self - { - return new self(($this->routes)($route)); - } - - /** - * @param Sequence> $routes - */ - public function append(Sequence $routes): self - { - return new self($this->routes->append($routes)); - } - - /** - * @internal - * - * @return Sequence> - */ - public function toSequence(): Sequence - { - return $this->routes; - } -} diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 04d5e24..8d39e62 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -19,11 +19,6 @@ Console, }; use Innmind\DI\Service; -use Innmind\Router\{ - Endpoint, - Method as RouterMethod, - Handle, -}; use Innmind\Http\{ ServerRequest, Response, @@ -849,24 +844,26 @@ public function testMatchRoutes(): BlackBox\Proof $responseB = Response::of(StatusCode::ok, $protocol); $app = Application::http(Factory::build(), Environment::test($variables)) - ->appendRoutes(fn($routes) => $routes->add( - RouterMethod::get() - ->pipe(Endpoint::of('/foo')) - ->pipe(Handle::via(function($request) use ($protocol, $responseA) { + ->route( + fn($pipe) => $pipe + ->get() + ->endpoint('/foo') + ->handle(function($request) use ($protocol, $responseA) { $this->assertSame($protocol, $request->protocolVersion()); return Attempt::result($responseA); - })), - )) - ->appendRoutes(fn($routes) => $routes->add( - RouterMethod::get() - ->pipe(Endpoint::of('/bar')) - ->pipe(Handle::via(function($request) use ($protocol, $responseB) { - $this->assertSame($protocol, $request->protocolVersion()); - - return Attempt::result($responseB); - })), - )); + }) + ->or( + $pipe + ->get() + ->endpoint('/bar') + ->handle(function($request) use ($protocol, $responseB) { + $this->assertSame($protocol, $request->protocolVersion()); + + return Attempt::result($responseB); + }), + ), + ); $response = $app->run(ServerRequest::of( Url::of('/foo'), @@ -1093,16 +1090,20 @@ public function testMatchMethodAllowed(): BlackBox\Proof ) ->prove(function($protocol, $variables) { $app = Application::http(Factory::build(), Environment::test($variables)) - ->appendRoutes(static fn($routes) => $routes->add( - Endpoint::of('/foo') - ->pipe(RouterMethod::get()) - ->pipe(Handle::of(static fn($request) => Attempt::result( - Response::of( - StatusCode::ok, - $request->protocolVersion(), - ), - ))), - )); + ->route( + static fn($pipe) => $pipe + ->endpoint('/foo') + ->any( + $pipe + ->get() + ->handle(static fn($request) => Attempt::result( + Response::of( + StatusCode::ok, + $request->protocolVersion(), + ), + )), + ), + ); $response = $app->run(ServerRequest::of( Url::of('/foo'), From 6e7693e9128f25b40f110f0baecdd5db1e90a071 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Sep 2025 13:25:57 +0200 Subject: [PATCH 03/12] add route declaration shortcut --- CHANGELOG.md | 4 + composer.json | 2 +- src/Http/Route.php | 244 ++++++++++++++++++++++++++++++++++++++ tests/ApplicationTest.php | 43 +++++++ 4 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 src/Http/Route.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d22f9fa..edf68de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- `Innmind\Framework\Http\Route` + ### Changed - Requires `innmind/foundation:~1.9` diff --git a/composer.json b/composer.json index a63cc87..dc083da 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "php": "~8.2", "innmind/foundation": "~1.9", "innmind/di": "~3.0", - "innmind/router": "~5.1" + "innmind/router": "~5.2" }, "autoload": { "psr-4": { diff --git a/src/Http/Route.php b/src/Http/Route.php new file mode 100644 index 0000000..98516a5 --- /dev/null +++ b/src/Http/Route.php @@ -0,0 +1,244 @@ + + */ + public static function get( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->get() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * + * @return callable(Pipe, Container): Component + */ + public static function post( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->post() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * + * @return callable(Pipe, Container): Component + */ + public static function put( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->put() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * + * @return callable(Pipe, Container): Component + */ + public static function patch( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->patch() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * + * @return callable(Pipe, Container): Component + */ + public static function delete( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->delete() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * + * @return callable(Pipe, Container): Component + */ + public static function options( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->options() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * + * @return callable(Pipe, Container): Component + */ + public static function trace( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->trace() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * + * @return callable(Pipe, Container): Component + */ + public static function connect( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->connect() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * + * @return callable(Pipe, Container): Component + */ + public static function head( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->head() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * + * @return callable(Pipe, Container): Component + */ + public static function link( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->link() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * + * @return callable(Pipe, Container): Component + */ + public static function unlink( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->unlink() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } +} diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 8d39e62..5926964 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -10,6 +10,7 @@ Middleware\Optional, Middleware\LoadDotEnv, Http\RequestHandler, + Http\Route, }; use Innmind\OperatingSystem\Factory; use Innmind\CLI\{ @@ -984,6 +985,48 @@ public function __invoke() }); } + public function testRouteToServiceShortcut(): BlackBox\Proof + { + return $this + ->forAll( + Set::of(...ProtocolVersion::cases()), + Set::sequence( + Set::compose( + static fn($key, $value) => [$key, $value], + Set::strings()->randomize(), + Set::strings(), + ), + )->between(0, 10), + ) + ->prove(function($protocol, $variables) { + $expected = Response::of(StatusCode::ok, $protocol); + + $app = Application::http(Factory::build(), Environment::test($variables)) + ->route(Route::get( + '/foo', + Services::responseHandler, + )) + ->service(Services::responseHandler, static fn() => new class($expected) { + public function __construct(private $response) + { + } + + public function __invoke() + { + return Attempt::result($this->response); + } + }); + + $response = $app->run(ServerRequest::of( + Url::of('/foo'), + Method::get, + $protocol, + )); + + $this->assertSame($expected, $response); + }); + } + public function testMapRequestHandler(): BlackBox\Proof { return $this From e24262bdbe0a4cd126834e1f17ed54021c35b433 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Sep 2025 13:47:14 +0200 Subject: [PATCH 04/12] add Application::mapRoute() --- CHANGELOG.md | 1 + src/Application.php | 12 +++++++ src/Application/Async/Http.php | 39 +++++++++++++++++++++-- src/Application/Cli.php | 9 ++++++ src/Application/Http.php | 39 +++++++++++++++++++++-- src/Application/Implementation.php | 9 ++++++ tests/ApplicationTest.php | 50 ++++++++++++++++++++++++++++++ 7 files changed, 153 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edf68de..0ede933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - `Innmind\Framework\Http\Route` +- `Innmind\Framework\Application::mapRoute()` ### Changed diff --git a/src/Application.php b/src/Application.php index a996076..020c893 100644 --- a/src/Application.php +++ b/src/Application.php @@ -168,6 +168,18 @@ public function mapRequestHandler(callable $map): self return new self($this->app->mapRequestHandler($map)); } + /** + * @psalm-mutation-free + * + * @param callable(Component, Container): Component $map + * + * @return self + */ + public function mapRoute(callable $map): self + { + return new self($this->app->mapRoute($map)); + } + /** * @psalm-mutation-free * diff --git a/src/Application/Async/Http.php b/src/Application/Async/Http.php index d7f3c5c..aafb656 100644 --- a/src/Application/Async/Http.php +++ b/src/Application/Async/Http.php @@ -49,6 +49,7 @@ final class Http implements Implementation * @param \Closure(OperatingSystem, Environment): Builder $container * @param Sequence> $routes * @param \Closure(RequestHandler, Container, OperatingSystem, Environment): RequestHandler $mapRequestHandler + * @param \Closure(Component, Container): Component $mapRoute * @param Maybe $notFound */ private function __construct( @@ -57,6 +58,7 @@ private function __construct( private \Closure $container, private Sequence $routes, private \Closure $mapRequestHandler, + private \Closure $mapRoute, private Maybe $notFound, ) { } @@ -75,6 +77,7 @@ public static function of(OperatingSystem $os): self static fn() => Builder::new(), Sequence::lazyStartingWith(), static fn(RequestHandler $handler) => $handler, + static fn(Component $component) => $component, $notFound, ); } @@ -98,6 +101,7 @@ static function(OperatingSystem $os, Environment $env) use ($previous, $map): ar $this->container, $this->routes, $this->mapRequestHandler, + $this->mapRoute, $this->notFound, ); } @@ -121,6 +125,7 @@ static function(OperatingSystem $os, Environment $env) use ($previous, $map): ar $this->container, $this->routes, $this->mapRequestHandler, + $this->mapRoute, $this->notFound, ); } @@ -142,6 +147,7 @@ public function service(Service $name, callable $definition): self ), $this->routes, $this->mapRequestHandler, + $this->mapRoute, $this->notFound, ); } @@ -176,6 +182,7 @@ public function route(callable $handle): self $this->container, ($this->routes)($handle), $this->mapRequestHandler, + $this->mapRoute, $this->notFound, ); } @@ -204,6 +211,29 @@ public function mapRequestHandler(callable $map): self $os, $env, ), + $this->mapRoute, + $this->notFound, + ); + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function mapRoute(callable $map): self + { + $previous = $this->mapRoute; + + return new self( + $this->os, + $this->map, + $this->container, + $this->routes, + $this->mapRequestHandler, + static fn($component, $get) => $map( + $previous($component, $get), + $get, + ), $this->notFound, ); } @@ -220,6 +250,7 @@ public function notFoundRequestHandler(callable $handle): self $this->container, $this->routes, $this->mapRequestHandler, + $this->mapRoute, Maybe::just($handle), ); } @@ -232,6 +263,7 @@ public function run($input) $routes = $this->routes; $notFound = $this->notFound; $mapRequestHandler = $this->mapRequestHandler; + $mapRoute = $this->mapRoute; $run = Commands::of(Serve::of( $this->os, @@ -241,14 +273,15 @@ static function(ServerRequest $request, OperatingSystem $os) use ( $routes, $notFound, $mapRequestHandler, + $mapRoute, ): Response { $env = Environment::http($request->environment()); [$os, $env] = $map($os, $env); $container = $container($os, $env)->build(); $pipe = Pipe::new(); - $routes = $routes->map( - static fn($handle) => $handle($pipe, $container, $os, $env), - ); + $routes = $routes + ->map(static fn($handle) => $handle($pipe, $container, $os, $env)) + ->map(static fn($component) => $mapRoute($component, $container)); $router = new Router( $routes, $notFound->map( diff --git a/src/Application/Cli.php b/src/Application/Cli.php index 2293e40..df8c360 100644 --- a/src/Application/Cli.php +++ b/src/Application/Cli.php @@ -172,6 +172,15 @@ public function mapRequestHandler(callable $map): self return $this; } + /** + * @psalm-mutation-free + */ + #[\Override] + public function mapRoute(callable $map): self + { + return $this; + } + /** * @psalm-mutation-free */ diff --git a/src/Application/Http.php b/src/Application/Http.php index bd15c57..5d44d81 100644 --- a/src/Application/Http.php +++ b/src/Application/Http.php @@ -40,6 +40,7 @@ final class Http implements Implementation * @param \Closure(OperatingSystem, Environment): Builder $container * @param Sequence> $routes * @param \Closure(RequestHandler, Container, OperatingSystem, Environment): RequestHandler $mapRequestHandler + * @param \Closure(Component, Container): Component $mapRoute * @param Maybe $notFound */ private function __construct( @@ -48,6 +49,7 @@ private function __construct( private \Closure $container, private Sequence $routes, private \Closure $mapRequestHandler, + private \Closure $mapRoute, private Maybe $notFound, ) { } @@ -66,6 +68,7 @@ public static function of(OperatingSystem $os, Environment $env): self static fn() => Builder::new(), Sequence::lazyStartingWith(), static fn(RequestHandler $handler) => $handler, + static fn(Component $component) => $component, $notFound, ); } @@ -83,6 +86,7 @@ public function mapEnvironment(callable $map): self $this->container, $this->routes, $this->mapRequestHandler, + $this->mapRoute, $this->notFound, ); } @@ -100,6 +104,7 @@ public function mapOperatingSystem(callable $map): self $this->container, $this->routes, $this->mapRequestHandler, + $this->mapRoute, $this->notFound, ); } @@ -121,6 +126,7 @@ public function service(Service $name, callable $definition): self ), $this->routes, $this->mapRequestHandler, + $this->mapRoute, $this->notFound, ); } @@ -155,6 +161,7 @@ public function route(callable $handle): self $this->container, ($this->routes)($handle), $this->mapRequestHandler, + $this->mapRoute, $this->notFound, ); } @@ -183,6 +190,29 @@ public function mapRequestHandler(callable $map): self $os, $env, ), + $this->mapRoute, + $this->notFound, + ); + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function mapRoute(callable $map): self + { + $previous = $this->mapRoute; + + return new self( + $this->os, + $this->env, + $this->container, + $this->routes, + $this->mapRequestHandler, + static fn($component, $get) => $map( + $previous($component, $get), + $get, + ), $this->notFound, ); } @@ -199,6 +229,7 @@ public function notFoundRequestHandler(callable $handle): self $this->container, $this->routes, $this->mapRequestHandler, + $this->mapRoute, Maybe::just($handle), ); } @@ -209,10 +240,12 @@ public function run($input) $container = ($this->container)($this->os, $this->env)->build(); $os = $this->os; $env = $this->env; + $mapRoute = $this->mapRoute; $pipe = Pipe::new(); - $routes = $this->routes->map( - static fn($handle) => $handle($pipe, $container, $os, $env), - ); + $routes = $this + ->routes + ->map(static fn($handle) => $handle($pipe, $container, $os, $env)) + ->map(static fn($component) => $mapRoute($component, $container)); $router = new Router( $routes, $this->notFound->map( diff --git a/src/Application/Implementation.php b/src/Application/Implementation.php index e895a00..f15dee9 100644 --- a/src/Application/Implementation.php +++ b/src/Application/Implementation.php @@ -99,6 +99,15 @@ public function route(callable $handle): self; */ public function mapRequestHandler(callable $map): self; + /** + * @psalm-mutation-free + * + * @param callable(Component, Container): Component $map + * + * @return self + */ + public function mapRoute(callable $map): self; + /** * @psalm-mutation-free * diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 5926964..683b08b 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -1158,4 +1158,54 @@ public function testMatchMethodAllowed(): BlackBox\Proof $this->assertSame($protocol, $response->protocolVersion()); }); } + + public function testMapRoute(): BlackBox\Proof + { + return $this + ->forAll( + Set::of(...ProtocolVersion::cases()), + Set::sequence( + Set::compose( + static fn($key, $value) => [$key, $value], + Set::strings()->randomize(), + Set::strings(), + ), + )->between(0, 10), + ) + ->prove(function($protocol, $variables) { + $response = Response::of(StatusCode::ok, $protocol); + $expected = Response::of(StatusCode::ok, $protocol); + + $app = Application::http(Factory::build(), Environment::test($variables)) + ->route(Route::get( + '/foo', + Services::responseHandler, + )) + ->mapRoute(fn($component) => $component->map( + function($out) use ($response, $expected) { + $this->assertSame($out, $response); + + return $expected; + }, + )) + ->service(Services::responseHandler, static fn() => new class($response) { + public function __construct(private $response) + { + } + + public function __invoke() + { + return Attempt::result($this->response); + } + }); + + $response = $app->run(ServerRequest::of( + Url::of('/foo'), + Method::get, + $protocol, + )); + + $this->assertSame($expected, $response); + }); + } } From 697e5d7ec5fbb7db628d77db362e84619a6bd636 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Sep 2025 13:57:48 +0200 Subject: [PATCH 05/12] remove RequestHandler interface --- CHANGELOG.md | 2 + src/Application.php | 13 ------- src/Application/Async/Http.php | 44 +--------------------- src/Application/Cli.php | 9 ----- src/Application/Http.php | 42 +-------------------- src/Application/Implementation.php | 14 +------ src/Http/RequestHandler.php | 14 ------- src/Http/Router.php | 3 +- tests/ApplicationTest.php | 59 ------------------------------ 9 files changed, 6 insertions(+), 194 deletions(-) delete mode 100644 src/Http/RequestHandler.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ede933..14d6cc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ - `Innmind\Framework\Http\To` - `Innmind\Framework\Http\Routes` - `Innmind\Framework\Application::appendRoutes()` +- `Innmind\Framework\Application::mapRequestHandler()` +- `Innmind\Framework\Http\RequestHandler` ### Fixed diff --git a/src/Application.php b/src/Application.php index 020c893..826380f 100644 --- a/src/Application.php +++ b/src/Application.php @@ -3,7 +3,6 @@ namespace Innmind\Framework; -use Innmind\Framework\Http\RequestHandler; use Innmind\OperatingSystem\OperatingSystem; use Innmind\CLI\{ Environment as CliEnv, @@ -156,18 +155,6 @@ public function route(callable $handle): self return new self($this->app->route($handle)); } - /** - * @psalm-mutation-free - * - * @param callable(RequestHandler, Container, OperatingSystem, Environment): RequestHandler $map - * - * @return self - */ - public function mapRequestHandler(callable $map): self - { - return new self($this->app->mapRequestHandler($map)); - } - /** * @psalm-mutation-free * diff --git a/src/Application/Async/Http.php b/src/Application/Async/Http.php index aafb656..face889 100644 --- a/src/Application/Async/Http.php +++ b/src/Application/Async/Http.php @@ -7,7 +7,6 @@ Environment, Application\Implementation, Http\Router, - Http\RequestHandler, }; use Innmind\CLI\{ Environment as CliEnv, @@ -48,7 +47,6 @@ final class Http implements Implementation * @param \Closure(OperatingSystem, Environment): array{OperatingSystem, Environment} $map * @param \Closure(OperatingSystem, Environment): Builder $container * @param Sequence> $routes - * @param \Closure(RequestHandler, Container, OperatingSystem, Environment): RequestHandler $mapRequestHandler * @param \Closure(Component, Container): Component $mapRoute * @param Maybe $notFound */ @@ -57,7 +55,6 @@ private function __construct( private \Closure $map, private \Closure $container, private Sequence $routes, - private \Closure $mapRequestHandler, private \Closure $mapRoute, private Maybe $notFound, ) { @@ -76,7 +73,6 @@ public static function of(OperatingSystem $os): self static fn(OperatingSystem $os, Environment $env) => [$os, $env], static fn() => Builder::new(), Sequence::lazyStartingWith(), - static fn(RequestHandler $handler) => $handler, static fn(Component $component) => $component, $notFound, ); @@ -100,7 +96,6 @@ static function(OperatingSystem $os, Environment $env) use ($previous, $map): ar }, $this->container, $this->routes, - $this->mapRequestHandler, $this->mapRoute, $this->notFound, ); @@ -124,7 +119,6 @@ static function(OperatingSystem $os, Environment $env) use ($previous, $map): ar }, $this->container, $this->routes, - $this->mapRequestHandler, $this->mapRoute, $this->notFound, ); @@ -146,7 +140,6 @@ public function service(Service $name, callable $definition): self static fn($service) => $definition($service, $os, $env), ), $this->routes, - $this->mapRequestHandler, $this->mapRoute, $this->notFound, ); @@ -181,36 +174,6 @@ public function route(callable $handle): self $this->map, $this->container, ($this->routes)($handle), - $this->mapRequestHandler, - $this->mapRoute, - $this->notFound, - ); - } - - /** - * @psalm-mutation-free - */ - #[\Override] - public function mapRequestHandler(callable $map): self - { - $previous = $this->mapRequestHandler; - - return new self( - $this->os, - $this->map, - $this->container, - $this->routes, - static fn( - RequestHandler $handler, - Container $container, - OperatingSystem $os, - Environment $env, - ) => $map( - $previous($handler, $container, $os, $env), - $container, - $os, - $env, - ), $this->mapRoute, $this->notFound, ); @@ -229,7 +192,6 @@ public function mapRoute(callable $map): self $this->map, $this->container, $this->routes, - $this->mapRequestHandler, static fn($component, $get) => $map( $previous($component, $get), $get, @@ -249,7 +211,6 @@ public function notFoundRequestHandler(callable $handle): self $this->map, $this->container, $this->routes, - $this->mapRequestHandler, $this->mapRoute, Maybe::just($handle), ); @@ -262,7 +223,6 @@ public function run($input) $container = $this->container; $routes = $this->routes; $notFound = $this->notFound; - $mapRequestHandler = $this->mapRequestHandler; $mapRoute = $this->mapRoute; $run = Commands::of(Serve::of( @@ -272,7 +232,6 @@ static function(ServerRequest $request, OperatingSystem $os) use ( $container, $routes, $notFound, - $mapRequestHandler, $mapRoute, ): Response { $env = Environment::http($request->environment()); @@ -293,9 +252,8 @@ static function(ServerRequest $request, OperatingSystem $os) use ( ), ), ); - $handle = $mapRequestHandler($router, $container, $os, $env); - return $handle($request); + return $router($request); }, )); diff --git a/src/Application/Cli.php b/src/Application/Cli.php index df8c360..149299c 100644 --- a/src/Application/Cli.php +++ b/src/Application/Cli.php @@ -163,15 +163,6 @@ public function route(callable $handle): self return $this; } - /** - * @psalm-mutation-free - */ - #[\Override] - public function mapRequestHandler(callable $map): self - { - return $this; - } - /** * @psalm-mutation-free */ diff --git a/src/Application/Http.php b/src/Application/Http.php index 5d44d81..c52ffad 100644 --- a/src/Application/Http.php +++ b/src/Application/Http.php @@ -6,7 +6,6 @@ use Innmind\Framework\{ Environment, Http\Router, - Http\RequestHandler, }; use Innmind\OperatingSystem\OperatingSystem; use Innmind\DI\{ @@ -39,7 +38,6 @@ final class Http implements Implementation * * @param \Closure(OperatingSystem, Environment): Builder $container * @param Sequence> $routes - * @param \Closure(RequestHandler, Container, OperatingSystem, Environment): RequestHandler $mapRequestHandler * @param \Closure(Component, Container): Component $mapRoute * @param Maybe $notFound */ @@ -48,7 +46,6 @@ private function __construct( private Environment $env, private \Closure $container, private Sequence $routes, - private \Closure $mapRequestHandler, private \Closure $mapRoute, private Maybe $notFound, ) { @@ -67,7 +64,6 @@ public static function of(OperatingSystem $os, Environment $env): self $env, static fn() => Builder::new(), Sequence::lazyStartingWith(), - static fn(RequestHandler $handler) => $handler, static fn(Component $component) => $component, $notFound, ); @@ -85,7 +81,6 @@ public function mapEnvironment(callable $map): self $map($this->env, $this->os), $this->container, $this->routes, - $this->mapRequestHandler, $this->mapRoute, $this->notFound, ); @@ -103,7 +98,6 @@ public function mapOperatingSystem(callable $map): self $this->env, $this->container, $this->routes, - $this->mapRequestHandler, $this->mapRoute, $this->notFound, ); @@ -125,7 +119,6 @@ public function service(Service $name, callable $definition): self static fn($service) => $definition($service, $os, $env), ), $this->routes, - $this->mapRequestHandler, $this->mapRoute, $this->notFound, ); @@ -160,36 +153,6 @@ public function route(callable $handle): self $this->env, $this->container, ($this->routes)($handle), - $this->mapRequestHandler, - $this->mapRoute, - $this->notFound, - ); - } - - /** - * @psalm-mutation-free - */ - #[\Override] - public function mapRequestHandler(callable $map): self - { - $previous = $this->mapRequestHandler; - - return new self( - $this->os, - $this->env, - $this->container, - $this->routes, - static fn( - RequestHandler $handler, - Container $container, - OperatingSystem $os, - Environment $env, - ) => $map( - $previous($handler, $container, $os, $env), - $container, - $os, - $env, - ), $this->mapRoute, $this->notFound, ); @@ -208,7 +171,6 @@ public function mapRoute(callable $map): self $this->env, $this->container, $this->routes, - $this->mapRequestHandler, static fn($component, $get) => $map( $previous($component, $get), $get, @@ -228,7 +190,6 @@ public function notFoundRequestHandler(callable $handle): self $this->env, $this->container, $this->routes, - $this->mapRequestHandler, $this->mapRoute, Maybe::just($handle), ); @@ -257,8 +218,7 @@ public function run($input) ), ), ); - $handle = ($this->mapRequestHandler)($router, $container, $this->os, $this->env); - return $handle($input); + return $router($input); } } diff --git a/src/Application/Implementation.php b/src/Application/Implementation.php index f15dee9..2484bb3 100644 --- a/src/Application/Implementation.php +++ b/src/Application/Implementation.php @@ -3,10 +3,7 @@ namespace Innmind\Framework\Application; -use Innmind\Framework\{ - Environment, - Http\RequestHandler, -}; +use Innmind\Framework\Environment; use Innmind\OperatingSystem\OperatingSystem; use Innmind\CLI\{ Environment as CliEnv, @@ -90,15 +87,6 @@ public function mapCommand(callable $map): self; */ public function route(callable $handle): self; - /** - * @psalm-mutation-free - * - * @param callable(RequestHandler, Container, OperatingSystem, Environment): RequestHandler $map - * - * @return self - */ - public function mapRequestHandler(callable $map): self; - /** * @psalm-mutation-free * diff --git a/src/Http/RequestHandler.php b/src/Http/RequestHandler.php deleted file mode 100644 index f9a2fd7..0000000 --- a/src/Http/RequestHandler.php +++ /dev/null @@ -1,14 +0,0 @@ -> $routes @@ -37,7 +37,6 @@ public function __construct( ) { } - #[\Override] public function __invoke(ServerRequest $request): Response { /** diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 683b08b..09d04f9 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -9,7 +9,6 @@ Environment, Middleware\Optional, Middleware\LoadDotEnv, - Http\RequestHandler, Http\Route, }; use Innmind\OperatingSystem\Factory; @@ -26,9 +25,7 @@ Method, Response\StatusCode, ProtocolVersion, - Header\ContentType, }; -use Innmind\MediaType\MediaType; use Innmind\Url\{ Url, Path, @@ -1027,62 +1024,6 @@ public function __invoke() }); } - public function testMapRequestHandler(): BlackBox\Proof - { - return $this - ->forAll( - FUrl::any(), - Set::of(...Method::cases()), - Set::of(...ProtocolVersion::cases()), - Set::sequence( - Set::compose( - static fn($key, $value) => [$key, $value], - Set::strings()->randomize(), - Set::strings(), - ), - )->between(0, 10), - ) - ->prove(function($url, $method, $protocol, $variables) { - $app = Application::http(Factory::build(), Environment::test($variables)) - ->mapRequestHandler(static fn($inner) => new class($inner) implements RequestHandler { - public function __construct( - private $inner, - ) { - } - - public function __invoke(ServerRequest $request): Response - { - $response = ($this->inner)($request); - - return Response::of( - $response->statusCode(), - $response->protocolVersion(), - $response->headers()(ContentType::of(new MediaType( - 'application', - 'octet-stream', - ))), - ); - } - }); - - $response = $app->run(ServerRequest::of( - $url, - $method, - $protocol, - )); - - $this->assertSame(StatusCode::notFound, $response->statusCode()); - $this->assertSame($protocol, $response->protocolVersion()); - $this->assertSame( - 'Content-Type: application/octet-stream', - $response->headers()->get('content-type')->match( - static fn($header) => $header->toString(), - static fn() => null, - ), - ); - }); - } - public function testAllowToSpecifyHttpNotFoundRequestHandler(): BlackBox\Proof { return $this From 1c3fb05b82f447519a4c3020a08eb3bb17028a37 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Sep 2025 14:01:17 +0200 Subject: [PATCH 06/12] allow the route not found handler to safely fail --- CHANGELOG.md | 1 + src/Application.php | 2 +- src/Application/Async/Http.php | 4 ++-- src/Application/Http.php | 5 +++-- src/Application/Implementation.php | 2 +- src/Http/Router.php | 4 ++-- tests/ApplicationTest.php | 2 +- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14d6cc4..87a0ad1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - `Innmind\Framework\Application::route()` callable must now return a `Innmind\Router\Component` - `Innmind\Framework\Application::route()` callable first parameter now is a `Innmint\Router\Pipe` - `Innmind\Framework\Application::route()` first parameter must now be expressed via a component inside the callable +- `Innminf\Framework\Application::notFoundRequestHandler()` callable must now return an `Innmind\Immutable\Attempt` ### Removed diff --git a/src/Application.php b/src/Application.php index 826380f..f8ecd0c 100644 --- a/src/Application.php +++ b/src/Application.php @@ -170,7 +170,7 @@ public function mapRoute(callable $map): self /** * @psalm-mutation-free * - * @param callable(ServerRequest, Container, OperatingSystem, Environment): Response $handle + * @param callable(ServerRequest, Container, OperatingSystem, Environment): Attempt $handle * * @return self */ diff --git a/src/Application/Async/Http.php b/src/Application/Async/Http.php index face889..d7206ea 100644 --- a/src/Application/Async/Http.php +++ b/src/Application/Async/Http.php @@ -48,7 +48,7 @@ final class Http implements Implementation * @param \Closure(OperatingSystem, Environment): Builder $container * @param Sequence> $routes * @param \Closure(Component, Container): Component $mapRoute - * @param Maybe $notFound + * @param Maybe> $notFound */ private function __construct( private OperatingSystem $os, @@ -65,7 +65,7 @@ private function __construct( */ public static function of(OperatingSystem $os): self { - /** @var Maybe */ + /** @var Maybe> */ $notFound = Maybe::nothing(); return new self( diff --git a/src/Application/Http.php b/src/Application/Http.php index c52ffad..2ddfd38 100644 --- a/src/Application/Http.php +++ b/src/Application/Http.php @@ -25,6 +25,7 @@ Maybe, Sequence, SideEffect, + Attempt, }; /** @@ -39,7 +40,7 @@ final class Http implements Implementation * @param \Closure(OperatingSystem, Environment): Builder $container * @param Sequence> $routes * @param \Closure(Component, Container): Component $mapRoute - * @param Maybe $notFound + * @param Maybe> $notFound */ private function __construct( private OperatingSystem $os, @@ -56,7 +57,7 @@ private function __construct( */ public static function of(OperatingSystem $os, Environment $env): self { - /** @var Maybe */ + /** @var Maybe> */ $notFound = Maybe::nothing(); return new self( diff --git a/src/Application/Implementation.php b/src/Application/Implementation.php index 2484bb3..1cb6186 100644 --- a/src/Application/Implementation.php +++ b/src/Application/Implementation.php @@ -99,7 +99,7 @@ public function mapRoute(callable $map): self; /** * @psalm-mutation-free * - * @param callable(ServerRequest, Container, OperatingSystem, Environment): Response $handle + * @param callable(ServerRequest, Container, OperatingSystem, Environment): Attempt $handle * * @return self */ diff --git a/src/Http/Router.php b/src/Http/Router.php index d07dbe8..b377cde 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -29,7 +29,7 @@ final class Router { /** * @param Sequence> $routes - * @param Maybe<\Closure(ServerRequest): Response> $notFound + * @param Maybe<\Closure(ServerRequest): Attempt> $notFound */ public function __construct( private Sequence $routes, @@ -47,7 +47,7 @@ public function __invoke(ServerRequest $request): Response ->otherwise(Respond::withHttpErrors()) ->or(Handle::via( fn($request, SideEffect $_) => $this->notFound->match( - static fn($handle) => Attempt::result($handle($request)), + static fn($handle) => $handle($request), static fn() => Attempt::result(Response::of( StatusCode::notFound, $request->protocolVersion(), diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 09d04f9..b202ac7 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -1046,7 +1046,7 @@ public function testAllowToSpecifyHttpNotFoundRequestHandler(): BlackBox\Proof ->notFoundRequestHandler(function($request) use ($protocol, $expected) { $this->assertSame($protocol, $request->protocolVersion()); - return $expected; + return Attempt::result($expected); }); $response = $app->run(ServerRequest::of( From 0c45e01b4073246af4276cb71cc5f4b7605e7ff0 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Sep 2025 14:04:01 +0200 Subject: [PATCH 07/12] rename notFoundRequestHandler to routeNotFound --- CHANGELOG.md | 1 + src/Application.php | 4 ++-- src/Application/Async/Http.php | 2 +- src/Application/Cli.php | 2 +- src/Application/Http.php | 2 +- src/Application/Implementation.php | 2 +- tests/ApplicationTest.php | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a0ad1..d99cabc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - `Innmind\Framework\Application::route()` callable first parameter now is a `Innmint\Router\Pipe` - `Innmind\Framework\Application::route()` first parameter must now be expressed via a component inside the callable - `Innminf\Framework\Application::notFoundRequestHandler()` callable must now return an `Innmind\Immutable\Attempt` +- `Innminf\Framework\Application::notFoundRequestHandler()` has been renamed `::routeNotFound()` ### Removed diff --git a/src/Application.php b/src/Application.php index f8ecd0c..abd2760 100644 --- a/src/Application.php +++ b/src/Application.php @@ -174,9 +174,9 @@ public function mapRoute(callable $map): self * * @return self */ - public function notFoundRequestHandler(callable $handle): self + public function routeNotFound(callable $handle): self { - return new self($this->app->notFoundRequestHandler($handle)); + return new self($this->app->routeNotFound($handle)); } /** diff --git a/src/Application/Async/Http.php b/src/Application/Async/Http.php index d7206ea..85aa436 100644 --- a/src/Application/Async/Http.php +++ b/src/Application/Async/Http.php @@ -204,7 +204,7 @@ public function mapRoute(callable $map): self * @psalm-mutation-free */ #[\Override] - public function notFoundRequestHandler(callable $handle): self + public function routeNotFound(callable $handle): self { return new self( $this->os, diff --git a/src/Application/Cli.php b/src/Application/Cli.php index 149299c..b65016c 100644 --- a/src/Application/Cli.php +++ b/src/Application/Cli.php @@ -176,7 +176,7 @@ public function mapRoute(callable $map): self * @psalm-mutation-free */ #[\Override] - public function notFoundRequestHandler(callable $handle): self + public function routeNotFound(callable $handle): self { return $this; } diff --git a/src/Application/Http.php b/src/Application/Http.php index 2ddfd38..d959d31 100644 --- a/src/Application/Http.php +++ b/src/Application/Http.php @@ -184,7 +184,7 @@ public function mapRoute(callable $map): self * @psalm-mutation-free */ #[\Override] - public function notFoundRequestHandler(callable $handle): self + public function routeNotFound(callable $handle): self { return new self( $this->os, diff --git a/src/Application/Implementation.php b/src/Application/Implementation.php index 1cb6186..dbb5cf6 100644 --- a/src/Application/Implementation.php +++ b/src/Application/Implementation.php @@ -103,7 +103,7 @@ public function mapRoute(callable $map): self; * * @return self */ - public function notFoundRequestHandler(callable $handle): self; + public function routeNotFound(callable $handle): self; /** * @param I $input diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index b202ac7..55ae6c3 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -1043,7 +1043,7 @@ public function testAllowToSpecifyHttpNotFoundRequestHandler(): BlackBox\Proof $expected = Response::of(StatusCode::ok, $protocol); $app = Application::http(Factory::build(), Environment::test($variables)) - ->notFoundRequestHandler(function($request) use ($protocol, $expected) { + ->routeNotFound(function($request) use ($protocol, $expected) { $this->assertSame($protocol, $request->protocolVersion()); return Attempt::result($expected); From ad60d1cb4125591fa205c5bd2848749c7eaca325 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Sep 2025 14:13:37 +0200 Subject: [PATCH 08/12] allow to define a route as an enum case --- CHANGELOG.md | 1 + src/Application.php | 8 ++++++-- src/Http/Route/Reference.php | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/Http/Route/Reference.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d99cabc..1997d87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - `Innmind\Framework\Http\Route` +- `Innmind\Framework\Http\Route\Reference` - `Innmind\Framework\Application::mapRoute()` ### Changed diff --git a/src/Application.php b/src/Application.php index abd2760..39ef25c 100644 --- a/src/Application.php +++ b/src/Application.php @@ -146,12 +146,16 @@ public function mapCommand(callable $map): self /** * @psalm-mutation-free * - * @param callable(Pipe, Container, OperatingSystem, Environment): Component $handle + * @param Http\Route\Reference|callable(Pipe, Container, OperatingSystem, Environment): Component $handle * * @return self */ - public function route(callable $handle): self + public function route(Http\Route\Reference|callable $handle): self { + if ($handle instanceof Http\Route\Reference) { + $handle = $handle->route(); + } + return new self($this->app->route($handle)); } diff --git a/src/Http/Route/Reference.php b/src/Http/Route/Reference.php new file mode 100644 index 0000000..a25e4e3 --- /dev/null +++ b/src/Http/Route/Reference.php @@ -0,0 +1,25 @@ + + */ + public function route(): callable; +} From d0ace910c81aaa0b0517358083f2bf07f066b69a Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Sep 2025 14:22:39 +0200 Subject: [PATCH 09/12] allow to register all route references at once --- CHANGELOG.md | 1 + src/Application.php | 18 +++++++++++++ tests/ApplicationTest.php | 54 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1997d87..9cc1f86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - `Innmind\Framework\Http\Route` - `Innmind\Framework\Http\Route\Reference` - `Innmind\Framework\Application::mapRoute()` +- `Innmind\Framework\Application::routes(class-string)` ### Changed diff --git a/src/Application.php b/src/Application.php index 39ef25c..9e7464e 100644 --- a/src/Application.php +++ b/src/Application.php @@ -159,6 +159,24 @@ public function route(Http\Route\Reference|callable $handle): self return new self($this->app->route($handle)); } + /** + * @psalm-mutation-free + * + * @param class-string $routes + * + * @return self + */ + public function routes(string $routes): self + { + $self = $this; + + foreach ($routes::cases() as $route) { + $self = $self->route($route); + } + + return $self; + } + /** * @psalm-mutation-free * diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 55ae6c3..cd38c8c 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -49,6 +49,20 @@ enum Services implements Service case serviceB; } +enum Routes implements Route\Reference +{ + case a; + case b; + + public function route(): callable + { + return match ($this) { + self::a => Route::get('/foo', Services::serviceA), + self::b => Route::get('/bar', Services::serviceB), + }; + } +} + class ApplicationTest extends TestCase { use BlackBox; @@ -1149,4 +1163,44 @@ public function __invoke() $this->assertSame($expected, $response); }); } + + public function testRoutesAsEnumCases(): BlackBox\Proof + { + return $this + ->forAll( + Set::of(...ProtocolVersion::cases()), + Set::sequence( + Set::compose( + static fn($key, $value) => [$key, $value], + Set::strings()->randomize(), + Set::strings(), + ), + )->between(0, 10), + ) + ->prove(function($protocol, $variables) { + $responseA = Response::of(StatusCode::ok, $protocol); + $responseB = Response::of(StatusCode::ok, $protocol); + + $app = Application::http(Factory::build(), Environment::test($variables)) + ->service(Services::serviceA, static fn() => static fn() => Attempt::result($responseA)) + ->service(Services::serviceB, static fn() => static fn() => Attempt::result($responseB)) + ->routes(Routes::class); + + $response = $app->run(ServerRequest::of( + Url::of('/foo'), + Method::get, + $protocol, + )); + + $this->assertSame($responseA, $response); + + $response = $app->run(ServerRequest::of( + Url::of('/bar'), + Method::get, + $protocol, + )); + + $this->assertSame($responseB, $response); + }); + } } From 77acb28280d2618fecb7ae3d173502bd4148b7df Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Sep 2025 14:29:32 +0200 Subject: [PATCH 10/12] force the kind of service to inject as a route handler --- src/Http/Route.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Http/Route.php b/src/Http/Route.php index 98516a5..12c75da 100644 --- a/src/Http/Route.php +++ b/src/Http/Route.php @@ -15,6 +15,7 @@ }; use Innmind\Http\Response; use Innmind\UrlTemplate\Template; +use Innmind\Immutable\Attempt; final class Route { @@ -26,6 +27,7 @@ private function __construct() * @psalm-pure * * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler * * @return callable(Pipe, Container): Component */ @@ -46,6 +48,7 @@ public static function get( * @psalm-pure * * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler * * @return callable(Pipe, Container): Component */ @@ -66,6 +69,7 @@ public static function post( * @psalm-pure * * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler * * @return callable(Pipe, Container): Component */ @@ -86,6 +90,7 @@ public static function put( * @psalm-pure * * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler * * @return callable(Pipe, Container): Component */ @@ -106,6 +111,7 @@ public static function patch( * @psalm-pure * * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler * * @return callable(Pipe, Container): Component */ @@ -126,6 +132,7 @@ public static function delete( * @psalm-pure * * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler * * @return callable(Pipe, Container): Component */ @@ -146,6 +153,7 @@ public static function options( * @psalm-pure * * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler * * @return callable(Pipe, Container): Component */ @@ -166,6 +174,7 @@ public static function trace( * @psalm-pure * * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler * * @return callable(Pipe, Container): Component */ @@ -186,6 +195,7 @@ public static function connect( * @psalm-pure * * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler * * @return callable(Pipe, Container): Component */ @@ -206,6 +216,7 @@ public static function head( * @psalm-pure * * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler * * @return callable(Pipe, Container): Component */ @@ -226,6 +237,7 @@ public static function link( * @psalm-pure * * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler * * @return callable(Pipe, Container): Component */ From 201e0abe78c7993a8e34f6359658c6e1ebbddb51 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Sep 2025 14:57:17 +0200 Subject: [PATCH 11/12] allow to recover route errors --- CHANGELOG.md | 1 + src/Application.php | 12 +++++++++++ src/Application/Async/Http.php | 33 ++++++++++++++++++++++++++++++ src/Application/Cli.php | 9 ++++++++ src/Application/Http.php | 32 +++++++++++++++++++++++++++++ src/Application/Implementation.php | 9 ++++++++ src/Http/Router.php | 7 +++++++ tests/ApplicationTest.php | 31 ++++++++++++++++++++++++++++ 8 files changed, 134 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cc1f86..32ce4ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - `Innmind\Framework\Http\Route\Reference` - `Innmind\Framework\Application::mapRoute()` - `Innmind\Framework\Application::routes(class-string)` +- `Innmind\Framework\Application::recoverRouteError()` ### Changed diff --git a/src/Application.php b/src/Application.php index 9e7464e..03b6290 100644 --- a/src/Application.php +++ b/src/Application.php @@ -201,6 +201,18 @@ public function routeNotFound(callable $handle): self return new self($this->app->routeNotFound($handle)); } + /** + * @psalm-mutation-free + * + * @param callable(ServerRequest, \Throwable, Container): Attempt $recover + * + * @return self + */ + public function recoverRouteError(callable $recover): self + { + return new self($this->app->recoverRouteError($recover)); + } + /** * @param I $input * diff --git a/src/Application/Async/Http.php b/src/Application/Async/Http.php index 85aa436..1ee4f1c 100644 --- a/src/Application/Async/Http.php +++ b/src/Application/Async/Http.php @@ -49,6 +49,7 @@ final class Http implements Implementation * @param Sequence> $routes * @param \Closure(Component, Container): Component $mapRoute * @param Maybe> $notFound + * @param \Closure(ServerRequest, \Throwable, Container): Attempt $recover */ private function __construct( private OperatingSystem $os, @@ -57,6 +58,7 @@ private function __construct( private Sequence $routes, private \Closure $mapRoute, private Maybe $notFound, + private \Closure $recover, ) { } @@ -75,6 +77,7 @@ public static function of(OperatingSystem $os): self Sequence::lazyStartingWith(), static fn(Component $component) => $component, $notFound, + static fn(ServerRequest $request, \Throwable $e) => Attempt::error($e), ); } @@ -98,6 +101,7 @@ static function(OperatingSystem $os, Environment $env) use ($previous, $map): ar $this->routes, $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -121,6 +125,7 @@ static function(OperatingSystem $os, Environment $env) use ($previous, $map): ar $this->routes, $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -142,6 +147,7 @@ public function service(Service $name, callable $definition): self $this->routes, $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -176,6 +182,7 @@ public function route(callable $handle): self ($this->routes)($handle), $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -197,6 +204,7 @@ public function mapRoute(callable $map): self $get, ), $this->notFound, + $this->recover, ); } @@ -213,6 +221,28 @@ public function routeNotFound(callable $handle): self $this->routes, $this->mapRoute, Maybe::just($handle), + $this->recover, + ); + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function recoverRouteError(callable $recover): self + { + $previous = $this->recover; + + return new self( + $this->os, + $this->map, + $this->container, + $this->routes, + $this->mapRoute, + $this->notFound, + static fn($request, $e, $container) => $previous($request, $e, $container)->recover( + static fn($e) => $recover($request, $e, $container), + ), ); } @@ -224,6 +254,7 @@ public function run($input) $routes = $this->routes; $notFound = $this->notFound; $mapRoute = $this->mapRoute; + $recover = $this->recover; $run = Commands::of(Serve::of( $this->os, @@ -233,6 +264,7 @@ static function(ServerRequest $request, OperatingSystem $os) use ( $routes, $notFound, $mapRoute, + $recover, ): Response { $env = Environment::http($request->environment()); [$os, $env] = $map($os, $env); @@ -251,6 +283,7 @@ static function(ServerRequest $request, OperatingSystem $os) use ( $env, ), ), + static fn($request, $e) => $recover($request, $e, $container), ); return $router($request); diff --git a/src/Application/Cli.php b/src/Application/Cli.php index b65016c..350ea25 100644 --- a/src/Application/Cli.php +++ b/src/Application/Cli.php @@ -181,6 +181,15 @@ public function routeNotFound(callable $handle): self return $this; } + /** + * @psalm-mutation-free + */ + #[\Override] + public function recoverRouteError(callable $recover): self + { + return $this; + } + #[\Override] public function run($input) { diff --git a/src/Application/Http.php b/src/Application/Http.php index d959d31..bbe3131 100644 --- a/src/Application/Http.php +++ b/src/Application/Http.php @@ -41,6 +41,7 @@ final class Http implements Implementation * @param Sequence> $routes * @param \Closure(Component, Container): Component $mapRoute * @param Maybe> $notFound + * @param \Closure(ServerRequest, \Throwable, Container): Attempt $recover */ private function __construct( private OperatingSystem $os, @@ -49,6 +50,7 @@ private function __construct( private Sequence $routes, private \Closure $mapRoute, private Maybe $notFound, + private \Closure $recover, ) { } @@ -67,6 +69,7 @@ public static function of(OperatingSystem $os, Environment $env): self Sequence::lazyStartingWith(), static fn(Component $component) => $component, $notFound, + static fn(ServerRequest $request, \Throwable $e) => Attempt::error($e), ); } @@ -84,6 +87,7 @@ public function mapEnvironment(callable $map): self $this->routes, $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -101,6 +105,7 @@ public function mapOperatingSystem(callable $map): self $this->routes, $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -122,6 +127,7 @@ public function service(Service $name, callable $definition): self $this->routes, $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -156,6 +162,7 @@ public function route(callable $handle): self ($this->routes)($handle), $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -177,6 +184,7 @@ public function mapRoute(callable $map): self $get, ), $this->notFound, + $this->recover, ); } @@ -193,6 +201,28 @@ public function routeNotFound(callable $handle): self $this->routes, $this->mapRoute, Maybe::just($handle), + $this->recover, + ); + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function recoverRouteError(callable $recover): self + { + $previous = $this->recover; + + return new self( + $this->os, + $this->env, + $this->container, + $this->routes, + $this->mapRoute, + $this->notFound, + static fn($request, $e, $container) => $previous($request, $e, $container)->recover( + static fn($e) => $recover($request, $e, $container), + ), ); } @@ -203,6 +233,7 @@ public function run($input) $os = $this->os; $env = $this->env; $mapRoute = $this->mapRoute; + $recover = $this->recover; $pipe = Pipe::new(); $routes = $this ->routes @@ -218,6 +249,7 @@ public function run($input) $env, ), ), + static fn($request, $e) => $recover($request, $e, $container), ); return $router($input); diff --git a/src/Application/Implementation.php b/src/Application/Implementation.php index dbb5cf6..0137121 100644 --- a/src/Application/Implementation.php +++ b/src/Application/Implementation.php @@ -105,6 +105,15 @@ public function mapRoute(callable $map): self; */ public function routeNotFound(callable $handle): self; + /** + * @psalm-mutation-free + * + * @param callable(ServerRequest, \Throwable, Container): Attempt $recover + * + * @return self + */ + public function recoverRouteError(callable $recover): self; + /** * @param I $input * diff --git a/src/Http/Router.php b/src/Http/Router.php index b377cde..d1c3ba9 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -30,21 +30,28 @@ final class Router /** * @param Sequence> $routes * @param Maybe<\Closure(ServerRequest): Attempt> $notFound + * @param \Closure(ServerRequest, \Throwable): Attempt $recover */ public function __construct( private Sequence $routes, private Maybe $notFound, + private \Closure $recover, ) { } public function __invoke(ServerRequest $request): Response { + $recover = $this->recover; + /** * @psalm-suppress MixedArgumentTypeCoercion */ $route = Route::of( Any::from($this->routes) ->otherwise(Respond::withHttpErrors()) + ->otherwise(static fn($e) => Handle::via( + static fn($request) => $recover($request, $e), + )) ->or(Handle::via( fn($request, SideEffect $_) => $this->notFound->match( static fn($handle) => $handle($request), diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index cd38c8c..4a86a20 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -1203,4 +1203,35 @@ public function testRoutesAsEnumCases(): BlackBox\Proof $this->assertSame($responseB, $response); }); } + + public function testRecoverRouteError(): BlackBox\Proof + { + return $this + ->forAll( + Set::of(...ProtocolVersion::cases()), + Set::sequence( + Set::compose( + static fn($key, $value) => [$key, $value], + Set::strings()->randomize(), + Set::strings(), + ), + )->between(0, 10), + ) + ->prove(function($protocol, $variables) { + $expected = Response::of(StatusCode::ok, $protocol); + + $app = Application::http(Factory::build(), Environment::test($variables)) + ->service(Services::serviceA, static fn() => static fn() => Attempt::error(new \Exception)) + ->recoverRouteError(static fn() => Attempt::result($expected)) + ->routes(Routes::class); + + $response = $app->run(ServerRequest::of( + Url::of('/foo'), + Method::get, + $protocol, + )); + + $this->assertSame($expected, $response); + }); + } } From d9d95716dfcd1a00caa9b98ab9315b9c1b766774 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Sep 2025 15:02:55 +0200 Subject: [PATCH 12/12] test route shortcut against all methods --- tests/ApplicationTest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 4a86a20..d273e9d 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -1000,6 +1000,7 @@ public function testRouteToServiceShortcut(): BlackBox\Proof { return $this ->forAll( + Set::of(...Method::cases()), Set::of(...ProtocolVersion::cases()), Set::sequence( Set::compose( @@ -1009,11 +1010,11 @@ public function testRouteToServiceShortcut(): BlackBox\Proof ), )->between(0, 10), ) - ->prove(function($protocol, $variables) { + ->prove(function($method, $protocol, $variables) { $expected = Response::of(StatusCode::ok, $protocol); $app = Application::http(Factory::build(), Environment::test($variables)) - ->route(Route::get( + ->route(Route::{$method->name}( '/foo', Services::responseHandler, )) @@ -1030,7 +1031,7 @@ public function __invoke() $response = $app->run(ServerRequest::of( Url::of('/foo'), - Method::get, + $method, $protocol, ));