diff --git a/CHANGELOG.md b/CHANGELOG.md index 27bc4d0..a59df65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Added + +- `Innmind\Framework\Application::mapRoutes()` + ## 4.0.2 - 2026-04-12 ### Fixed diff --git a/src/Application.php b/src/Application.php index 2e30aab..9e94aa3 100644 --- a/src/Application.php +++ b/src/Application.php @@ -189,6 +189,11 @@ public function routes(string $routes): self } /** + * This wraps each route individually. + * + * This means that the code may be executed multiple times until it reaches + * the appropriate route. + * * @psalm-mutation-free * * @param callable(Component, Container): Component $map @@ -201,6 +206,23 @@ public function mapRoute(callable $map): self return new self($this->app->mapRoute($map)); } + /** + * This wraps all routes as whole. + * + * This means that the code will be executed only once. + * + * @psalm-mutation-free + * + * @param callable(Component, Container): Component $map + * + * @return self + */ + #[\NoDiscard] + public function mapRoutes(callable $map): self + { + return new self($this->app->mapRoutes($map)); + } + /** * @psalm-mutation-free * diff --git a/src/Application/Async/Http.php b/src/Application/Async/Http.php index b11d560..d865200 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(Component, Container): Component $mapRoute + * @param \Closure(Component, Container): Component $mapRoutes * @param Maybe> $notFound * @param \Closure(ServerRequest, \Throwable, Container): Attempt $recover */ @@ -58,6 +59,7 @@ private function __construct( private \Closure $container, private Sequence $routes, private \Closure $mapRoute, + private \Closure $mapRoutes, private Maybe $notFound, private \Closure $recover, ) { @@ -77,6 +79,7 @@ public static function of(OperatingSystem $os): self static fn() => Builder::new(), Sequence::lazyStartingWith(), static fn(Component $component) => $component, + static fn(Component $component) => $component, $notFound, static fn(ServerRequest $request, \Throwable $e) => Attempt::error($e), ); @@ -101,6 +104,7 @@ static function(OperatingSystem $os, Environment $env) use ($previous, $map): ar $this->container, $this->routes, $this->mapRoute, + $this->mapRoutes, $this->notFound, $this->recover, ); @@ -125,6 +129,7 @@ static function(OperatingSystem $os, Environment $env) use ($previous, $map): ar $this->container, $this->routes, $this->mapRoute, + $this->mapRoutes, $this->notFound, $this->recover, ); @@ -147,6 +152,7 @@ public function service(Service $name, callable $definition): self ), $this->routes, $this->mapRoute, + $this->mapRoutes, $this->notFound, $this->recover, ); @@ -182,6 +188,7 @@ public function route(callable $handle): self $this->container, ($this->routes)($handle), $this->mapRoute, + $this->mapRoutes, $this->notFound, $this->recover, ); @@ -204,6 +211,30 @@ public function mapRoute(callable $map): self $previous($component, $get), $get, ), + $this->mapRoutes, + $this->notFound, + $this->recover, + ); + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function mapRoutes(callable $map): self + { + $previous = $this->mapRoutes; + + return new self( + $this->os, + $this->map, + $this->container, + $this->routes, + $this->mapRoute, + static fn($component, $get) => $map( + $previous($component, $get), + $get, + ), $this->notFound, $this->recover, ); @@ -221,6 +252,7 @@ public function routeNotFound(callable $handle): self $this->container, $this->routes, $this->mapRoute, + $this->mapRoutes, Maybe::just($handle), $this->recover, ); @@ -240,6 +272,7 @@ public function recoverRouteError(callable $recover): self $this->container, $this->routes, $this->mapRoute, + $this->mapRoutes, $this->notFound, static fn($request, $e, $container) => $previous($request, $e, $container)->recover( static fn($e) => $recover($request, $e, $container), @@ -255,6 +288,7 @@ public function run($input): Attempt $routes = $this->routes; $notFound = $this->notFound; $mapRoute = $this->mapRoute; + $mapRoutes = $this->mapRoutes; $recover = $this->recover; $run = Commands::of(Serve::of( @@ -265,6 +299,7 @@ static function(ServerRequest $request, OperatingSystem $os, Map $env) use ( $routes, $notFound, $mapRoute, + $mapRoutes, $recover, ): Response { $env = Environment::of($env); @@ -275,6 +310,7 @@ static function(ServerRequest $request, OperatingSystem $os, Map $env) use ( ->map(static fn($handle) => $handle($pipe, $container)) ->map(static fn($component) => $mapRoute($component, $container)); $router = new Router( + static fn($component) => $mapRoutes($component, $container), $routes, $notFound->map( static fn($handle) => static fn(ServerRequest $request) => $handle( diff --git a/src/Application/Cli.php b/src/Application/Cli.php index d5bd9b1..f125e5a 100644 --- a/src/Application/Cli.php +++ b/src/Application/Cli.php @@ -178,6 +178,15 @@ public function mapRoute(callable $map): self return $this; } + /** + * @psalm-mutation-free + */ + #[\Override] + public function mapRoutes(callable $map): self + { + return $this; + } + /** * @psalm-mutation-free */ diff --git a/src/Application/Http.php b/src/Application/Http.php index 120aeb0..8f7d858 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(Component, Container): Component $mapRoute + * @param \Closure(Component, Container): Component $mapRoutes * @param Maybe> $notFound * @param \Closure(ServerRequest, \Throwable, Container): Attempt $recover */ @@ -49,6 +50,7 @@ private function __construct( private \Closure $container, private Sequence $routes, private \Closure $mapRoute, + private \Closure $mapRoutes, private Maybe $notFound, private \Closure $recover, ) { @@ -68,6 +70,7 @@ public static function of(OperatingSystem $os, Environment $env): self static fn() => Builder::new(), Sequence::lazyStartingWith(), static fn(Component $component) => $component, + static fn(Component $component) => $component, $notFound, static fn(ServerRequest $request, \Throwable $e) => Attempt::error($e), ); @@ -86,6 +89,7 @@ public function mapEnvironment(callable $map): self $this->container, $this->routes, $this->mapRoute, + $this->mapRoutes, $this->notFound, $this->recover, ); @@ -104,6 +108,7 @@ public function mapOperatingSystem(callable $map): self $this->container, $this->routes, $this->mapRoute, + $this->mapRoutes, $this->notFound, $this->recover, ); @@ -126,6 +131,7 @@ public function service(Service $name, callable $definition): self ), $this->routes, $this->mapRoute, + $this->mapRoutes, $this->notFound, $this->recover, ); @@ -161,6 +167,7 @@ public function route(callable $handle): self $this->container, ($this->routes)($handle), $this->mapRoute, + $this->mapRoutes, $this->notFound, $this->recover, ); @@ -183,6 +190,30 @@ public function mapRoute(callable $map): self $previous($component, $get), $get, ), + $this->mapRoutes, + $this->notFound, + $this->recover, + ); + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function mapRoutes(callable $map): self + { + $previous = $this->mapRoutes; + + return new self( + $this->os, + $this->env, + $this->container, + $this->routes, + $this->mapRoute, + static fn($component, $get) => $map( + $previous($component, $get), + $get, + ), $this->notFound, $this->recover, ); @@ -200,6 +231,7 @@ public function routeNotFound(callable $handle): self $this->container, $this->routes, $this->mapRoute, + $this->mapRoutes, Maybe::just($handle), $this->recover, ); @@ -219,6 +251,7 @@ public function recoverRouteError(callable $recover): self $this->container, $this->routes, $this->mapRoute, + $this->mapRoutes, $this->notFound, static fn($request, $e, $container) => $previous($request, $e, $container)->recover( static fn($e) => $recover($request, $e, $container), @@ -231,6 +264,7 @@ public function run($input): Attempt { $container = ($this->container)($this->os, $this->env)->build(); $mapRoute = $this->mapRoute; + $mapRoutes = $this->mapRoutes; $recover = $this->recover; $pipe = Pipe::new(); $routes = $this @@ -238,6 +272,7 @@ public function run($input): Attempt ->map(static fn($handle) => $handle($pipe, $container)) ->map(static fn($component) => $mapRoute($component, $container)); $router = new Router( + static fn($component) => $mapRoutes($component, $container), $routes, $this->notFound->map( static fn($handle) => static fn(ServerRequest $request) => $handle( diff --git a/src/Application/Implementation.php b/src/Application/Implementation.php index 9cccfd4..2ffb688 100644 --- a/src/Application/Implementation.php +++ b/src/Application/Implementation.php @@ -96,6 +96,15 @@ public function route(callable $handle): self; */ public function mapRoute(callable $map): self; + /** + * @psalm-mutation-free + * + * @param callable(Component, Container): Component $map + * + * @return self + */ + public function mapRoutes(callable $map): self; + /** * @psalm-mutation-free * diff --git a/src/Http/Router.php b/src/Http/Router.php index ada236e..94ef7f8 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -29,11 +29,13 @@ final class Router { /** + * @param \Closure(Component): Component $mapRoutes * @param Sequence> $routes * @param Maybe<\Closure(ServerRequest): Attempt> $notFound * @param \Closure(ServerRequest, \Throwable): Attempt $recover */ public function __construct( + private \Closure $mapRoutes, private Sequence $routes, private Maybe $notFound, private \Closure $recover, @@ -52,7 +54,7 @@ public function __invoke(ServerRequest $request): Attempt * @psalm-suppress MixedArgumentTypeCoercion */ $route = Route::of( - Any::from($this->routes) + ($this->mapRoutes)(Any::from($this->routes)) ->mapError(static fn($e) => match (true) { $e instanceof NoRouteProvided => new NotFound, default => $e, diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 25b1296..7fa41be 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -19,6 +19,7 @@ Command\Usage, Console, }; +use Innmind\Router\Component; use Innmind\DI\Service; use Innmind\Http\{ ServerRequest, @@ -1441,4 +1442,73 @@ public function testRouteErrorIsNotSwallowed(): BlackBox\Proof $this->assertSame($expected, $error); }); } + + public function testMapRoutes(): 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) { + $count = 0; + $app = Application::http(Factory::build(), Environment::test($variables)) + ->service(Services::serviceA, static fn() => static fn($request) => Attempt::result(Response::of( + StatusCode::ok, + $request->protocolVersion(), + ))) + ->service(Services::serviceB, static fn() => static fn($request) => Attempt::result(Response::of( + StatusCode::ok, + $request->protocolVersion(), + ))) + ->routes(Routes::class) + ->mapRoutes( + static function($previous) use (&$count) { + return Component::of( + static function($_, $input) use (&$count) { + ++$count; + + return Attempt::result($input); + }, + )->pipe($previous); + }, + ); + + $result = $app + ->run(ServerRequest::of( + Url::of('/foo'), + Method::get, + $protocol, + )) + ->match( + static fn() => true, + static fn() => false, + ); + + $this->assertTrue($result); + $this->assertSame(1, $count); + + $count = 0; + + $result = $app + ->run(ServerRequest::of( + Url::of('/bar'), + Method::get, + $protocol, + )) + ->match( + static fn() => true, + static fn() => false, + ); + + $this->assertTrue($result); + $this->assertSame(1, $count); + }); + } }