From 5d7f59bc99b39474c19e288a89acb0c9be583e78 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 20 Sep 2025 12:12:18 +0200 Subject: [PATCH 1/2] fix readme --- README.md | 89 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 0105c58..d5427d1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ Minimalist HTTP/CLI framework that accomodate to simple applications to complex The framework configuration is immutable and use a declarative approach. -**Important**: to correctly use this library you must validate your code with [`vimeo/psalm`](https://packagist.org/packages/vimeo/psalm) +> [!IMPORTANT] +> to correctly use this library you must validate your code with [`vimeo/psalm`](https://packagist.org/packages/vimeo/psalm) ## Installation @@ -36,30 +37,45 @@ require 'path/to/composer/autoload.php'; use Innmind\Framework\{ Main\Http, Application, + Http\Route, }; -use Innmind\Router\Route\Variables; +use Innmind\DI\Service; use Innmind\Http\{ ServerRequest, Response, Response\StatusCode, }; use Innmind\Filesystem\File\Content; +use Innmind\Immutable\Attempt; + +enum Services implements Service +{ + case hello; +} new class extends Http { protected function configure(Application $app): Application { return $app - ->route('GET /', static fn(ServerRequest $request) => Response::of( + ->service(Services::hello, static fn() => static fn( + ServerRequest $request, + ?string $name = null, + ) => Attempt::result(Response::of( StatusCode::ok, $request->protocolVersion(), null, - Content::ofString('Hello world!'), + Content::ofString(\sprintf( + 'Hello %s!', + $name ?? 'world', + )), + ))) + ->route(Route::get( + '/', + Services::hello, )) - ->route('GET /{name}', static fn(ServerRequest $request, Variables $variables) => Response::of( - StatusCode::ok, - $request->protocolVersion(), - null, - Content::ofString("Hello {$variables->get('name')}!"), + ->route(Route::get( + '/{name}', + Services::hello, )); } }; @@ -87,38 +103,49 @@ use Innmind\Framework\{ use Innmind\OperatingSystem\OperatingSystem; use Innmind\TimeContinuum\{ Clock, - Earth\Format\ISO8601, + Format, }; use Innmind\DI\Container; use Innmind\CLI\{ Console, Command, + Command\Usage, }; -use Innmind\Immutable\Str; +use Innmind\Immutable\{ + Attempt, + Str, +}; + +enum Services implements Service +{ + case clock; +} new class extends Cli { protected function configure(Application $app): Application { - return $app->command( - static fn(Container $container, OperatingSystem $os) => new class($os->clock()) implements Command { - public function __construct( - private Clock $clock, - ) { - } - - public function __invoke(Console $console): Console - { - $today = $this->clock->now()->format(new ISO8601); - - return $console->output(Str::of("We are the: $today\n")); - } - - public function usage(): string - { - return 'today'; - } - }, - ); + return $app + ->service(Services::clock, static fn($_, OperatingSystem $os) => $os->clock()) + ->command( + static fn(Container $container) => new class($container(Services::clock)) implements Command { + public function __construct( + private Clock $clock, + ) { + } + + public function __invoke(Console $console): Attempt + { + $today = $this->clock->now()->format(Format::iso8601()); + + return $console->output(Str::of("We are the: $today\n")); + } + + public function usage(): Usage + { + return Usage::of('today'); + } + }, + ); } }; ``` From 5d594b99c1415fe0c226b0c0ac49b7c8f6e1bc7a Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 20 Sep 2025 14:14:17 +0200 Subject: [PATCH 2/2] update documentation --- docs/cli.md | 58 +++--- docs/experimental/async-server.md | 2 +- docs/http-and-cli.md | 22 +-- docs/http.md | 316 +++++++++--------------------- docs/middlewares.md | 15 +- docs/operating-system.md | 6 +- docs/services.md | 24 +-- docs/testing.md | 50 ++--- 8 files changed, 175 insertions(+), 318 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index c5924d9..f1feb82 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -25,7 +25,7 @@ By default this application will write `Hello world` when you call `php entrypoi ## Handle commands -This example reuses the AMQP clients defined in the [services topic](services.md). +This example reuses the AMQP clients defined in the [services section](services.md). ```php use Innmind\Framework\{ @@ -35,6 +35,7 @@ use Innmind\Framework\{ use Innmind\CLI\{ Console, Command, + Command\Usage, }; use Innmind\DI\{ Container, @@ -46,7 +47,10 @@ use Innmind\AMQP\{ Command\Get, Model\Basic\Message, }; -use Innmind\Immutable\Str; +use Innmind\Immutable\{ + Attempt, + Str, +}; enum Services implements Service { @@ -58,15 +62,15 @@ new class extends Cli { protected function configure(Application $app): Application { return $app - ->service(Services::producerClient, /* see services topic */) - ->service(Services::consumerClient, /* see services topic */) + ->service(Services::producerClient, /* see services section */) + ->service(Services::consumerClient, /* see services section */) ->command(static fn(Container $container) => new class($container(Services::producerClient)) implements Command { public function __construct( private Client $amqp, ) { } - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { $message = Message::of(Str::of( $console->arguments()->get('url'), @@ -76,19 +80,17 @@ new class extends Cli { ->client ->with(Publish::one($message)->to('some-exchange')) ->run($console) - ->match( - static fn($console) => $console->output( - Str::of("Message published\n"), - ), - static fn() => $console->error( - Str::of("Something went wrong\n"), - ), - ); + ->flatMap(static fn($console) => $console->output( + Str::of("Message published\n"), + )) + ->recover(static fn() => $console->error( + Str::of("Something went wrong\n"), + )); } - public function usage(): string + public function usage(): Usage { - return 'publish url'; + return Usage::of('publish')->argument('url'); } }) ->command(static fn(Container $container) => new class($container(Services::consumerClient)) implements Command { @@ -97,25 +99,23 @@ new class extends Cli { ) { } - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $this ->client ->with(Get::of('some-queue')) ->run($console) - ->match( - static fn($console) => $console->output( - Str::of("One message pulled from queue\n"), - ), - static fn() => $console->error( - Str::of("Something went wrong\n"), - ), - ); + ->flatMap(static fn($console) => $console->output( + Str::of("One message pulled from queue\n"), + )) + ->recover(static fn() => $console->error( + Str::of("Something went wrong\n"), + )); } - public function usage(): string + public function usage(): Usage { - return 'consume'; + return Usage::of('consume'); } }); } @@ -140,7 +140,9 @@ use Innmind\Framework\{ use Innmind\CLI\{ Console, Command, + Command\Usage, }; +use Innmind\Immutable\Attempt; new class extends Cli { protected function configure(Application $app): Application @@ -153,14 +155,14 @@ new class extends Cli { ) { } - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { // do something before the real command return ($this->inner)($console); } - public function usage(): string + public function usage(): Usage { return $this->inner->usage(); } diff --git a/docs/experimental/async-server.md b/docs/experimental/async-server.md index 5258470..a2a4780 100644 --- a/docs/experimental/async-server.md +++ b/docs/experimental/async-server.md @@ -7,7 +7,7 @@ The framework comes with an HTTP server entirely built in PHP allowing you to se To use it is similar to the standard [http](../http.md) handler, the first difference is the namespace of the main entrypoint: -```php +```php title="index.php" hl_lines="7" $os ->filesystem() - ->mount(Path::of('somewhere/on/the/filesystem/')), + ->mount(Path::of('somewhere/on/the/filesystem/')) + ->unwrap(), ) - ->service(Services::amqp, /* see services topic */) + ->service(Services::amqp, /* see services section */) ->service(Services::upload, static fn(Container $container) => new UploadHandler( //(1) $container(Services::images), $container(Services::amqp), )) - ->appendRoutes( - static fn(Routes $routes, Container $container) => $routes->add( - Route::literal('POST /upload')->handle(Service::of($container, Services::upload)), - ), - ) + ->route(Route::post( + '/upload', + Services::upload, + )) ->command(static fn(Container $container) => new ThumbnailWorker( //(2) $container(Services::images), $container(Services::amqp), @@ -90,4 +88,4 @@ new class extends Http { } ``` -In the case on the CLI the call to `appendRoutes` will have no effect and for HTTP `command` will have no effect. +In the case on the CLI the call to `route` will have no effect and for HTTP `command` will have no effect. diff --git a/docs/http.md b/docs/http.md index 37ba428..6adddbe 100644 --- a/docs/http.md +++ b/docs/http.md @@ -2,7 +2,7 @@ The first of any HTTP app is to create an `index.php` that will be exposed via a web server. -```php +```php title="index.php" appendRoutes( - static fn(Routes $routes) => $routes - ->add(Route::literal('GET /')) - ->add(Route::literal('GET /{name}')), - ); - } -}; -``` - -This example defines 2 routes both accessible via a `GET` method. But this doesn't do much as we didn't specify what to do when they're called (the default behaviour is `200 Ok` with an empty response body). - -To specify a behaviour you need to attach a handler on each route. - -```php -use Innmind\Framework\{ - Main\Http, - Application, - Http\Routes, -}; -use Innmind\Router\{ - Route, - Route\Variables, + Http\Route, }; +use Innmind\DI\Service; use Innmind\Http\{ ServerRequest, Response, Response\StatusCode, }; use Innmind\Filesystem\File\Content; +use Innmind\Immutable\Attempt; + +enum Services implements Service +{ + case hello; +} new class extends Http { protected function configure(Application $app): Application { - return $app->appendRoutes( - static fn(Routes $routes) => $routes - ->add(Route::literal('GET /')->handle( - static fn(ServerRequest $request) => Response::of( - StatusCode::ok, - $request->protocolVersion(), - null, - Content::ofString('Hello world!'), - ), - )) - ->add(Route::literal('GET /{name}')->handle( - static fn( - ServerRequest $request, - Variables $variables, - ) => Response::of( - StatusCode::ok, - $request->protocolVersion(), - null, - Content::ofString("Hello {$variables->get('name')}!"), - ), + return $app + ->service(Services::hello, static fn() => static fn( + ServerRequest $request, + ?string $name = null, + ) => Attempt::result(Response::of( + StatusCode::ok, + $request->protocolVersion(), + null, + Content::ofString(\sprintf( + 'Hello %s!', + $name ?? 'world', )), - ); + ))) + ->route(Route::get( + '/', + Services::hello, + )) + ->route(Route::get( + '/{name}', + Services::hello, + )); } }; ``` -For simple apps having the whole behaviour next to the route can be ok. But like in this case it can be repetitive, for such case we can specify our behaviours elsewhere: [services](#services). - -## Multiple methods for the same path - -For REST apis it is common to implements differents methods for the same path in a CRUD like fashion. To avoid duplicating te template for each route you can regroup your routes like this: - -```php -use Innmind\Framework\{ - Main\Http, - Application, - Http\Routes, -}; -use Innmind\Router\Under; -use Innmind\Http\{ - ServerRequest, - Method, - Response, - Response\StatusCode, -}; -use Innmind\UrlTemplate\Template; -use Innmind\Filesystem\File\Content; - -new class extends Http { - protected function configure(Application $app): Application - { - return $app->appendRoutes( - static fn(Routes $routes) => $routes->add( - Under::of(Template::of('/some/resource/{id}')) - ->route(Method::get, static fn($route) => $route->handle( - static fn(ServerRequest $request) => Response::of( - StatusCode::ok, - $request->protocolVersion(), - null, - Content::ofString('{"id": 42, "name": "resource"}'), - ), - )) - ->route(Method::delete, static fn($route) => $route->handle( - static fn(ServerRequest $request) => Response::of( - StatusCode::noContent, - $request->protocolVersion(), - ), - )) - ), - ); - } -}; -``` +This example defines 2 routes both accessible via a `GET` method. When called, a route will be handled by the `Services::hello` service. -The other advantage to grouping your routes this way is that when a request matches the path but no method is defined then the framework will automatically respond a `405 Method Not Allowed`. +For simplicity here the route handler is defined as a `Closure` but you can use objects instead. -## Short syntax +## Multiple methods for the same path -The previous shows the default way to declare routes, but for very simple apps it can be a bit verbose. The framework provides a shorter syntax to handle routes: +For REST apis it is common to implements differents methods for the same path in a CRUD like fashion. To avoid duplicating the template for each route you can regroup your routes like this: ```php use Innmind\Framework\{ Main\Http, Application, }; -use Innmind\Router\Route\Variables; +use Innmind\DI\Container; use Innmind\Http\{ ServerRequest, Response, Response\StatusCode, }; use Innmind\Filesystem\File\Content; - -new class extends Http { - protected function configure(Application $app): Application - { - return $app - ->route( - 'GET /', - static fn(ServerRequest $request) => Response::of( - StatusCode::ok, - $request->protocolVersion(), - null, - Content::ofString('Hello world!'), - ), - ) - ->route( - 'GET /{name}', - static fn( - ServerRequest $request, - Variables $variables, - ) => Response::of( - StatusCode::ok, - $request->protocolVersion(), - null, - Content::ofString("Hello {$variables->get('name')}!"), - ), - ); - } -}; -``` - -## Services - -Services are any object that are referenced by a string in a [`Container`](https://github.com/Innmind/DI). For example let's take the route handler from the previous section and move them inside services. - -```php -use Innmind\Framework\{ - Main\Http, - Application, - Http\Routes, - Http\Service, - Http\To, -}; -use Innmind\DI\{ - Container, - Service, -}; -use Innmind\Router\{ - Route, - Route\Variables, -}; -use Innmind\Http\Message\{ - ServerRequest, - Response, - Response\StatusCode, -}; -use Innmind\Filesystem\File\Content; +use Innmind\Immutable\Attempt; enum Services implements Service { - case helloWorld; - case helloName; + case get; + case delete; } new class extends Http { @@ -229,108 +106,94 @@ new class extends Http { { return $app ->service( - Services::helloWorld, - static fn() => new class { - public function __invoke(ServerRequest $request): Response - { - return Response::of( - StatusCode::ok, - $request->protocolVersion(), - null, - Content::ofString('Hello world!'), - ); - } - } + Services::get, + static fn() => static fn(ServerRequest $request) => Attempt::result( + Response::of( + StatusCode::ok, + $request->protocolVersion(), + null, + Content::ofString('{"id": 42, "name": "resource"}'), + ), + ), ) ->service( - Services::helloName, - static fn() => new class { - public function __invoke( - ServerRequest $request, - Variables $variables, - ): Response { - return Response::of( - StatusCode::ok, - $request->protocolVersion(), - null, - Content::ofString("Hello {$variables->get('name')}!"), - ); - } - } - ) - ->appendRoutes( - static fn(Routes $routes, Container $container) => $routes->add( - Route::literal('GET /')->handle( - Service::of($container, Services::helloWorld), + Services::delete, + static fn() => static fn(ServerRequest $request) => Attempt::result( + Response::of( + StatusCode::noContent, + $request->protocolVersion(), ), ), ) - ->route('GET /{name}', To::service(Services::helloName)); + ->route( + static fn(Pipe $pipe, Container $container) => $pipe + ->endpoint('/some/resource/{id}') + ->any( + $pipe + ->forward() + ->get() + ->spread() + ->handle($container(Services::get))), + $pipe + ->forward() + ->delete() + ->spread() + ->handle($container(Services::delete))), + ), + ); } }; ``` -Here the services are invokable anonymous classes to conform to the callable expected for a `Route` but you can create dedicated classes for each one. - -!!! note "" - Head to the [services topic](services.md) for a more in-depth look of what's possible. +The other advantage to grouping your routes this way is that when a request matches the path but no method is defined then the framework will automatically respond a `405 Method Not Allowed`. ## Executing code on any route -Sometimes you want to execute some code on every route (like verifying the request is authenticated). So far your only approach would be to use inheritance on each route handler but this leads to bloated code. - -Fortunately there is better approach: composition of `RequestHandler`s. +Sometimes you want to execute some code on every route (like verifying the request is authenticated). ```php use Innmind\Framework\{ Main\Http, Application, - Http\RequestHandler, }; +use Innmind\Router\Component; use Innmind\Http\Message\{ ServerRequest, Response, Response\StatusCode, }; +use Innmind\Immutable\Attempt; new class extends Http { protected function configure(Application $app): Application { return $app - ->mapRequestHandler( - static fn(RequestHandler $handler) => new class($handler) implements RequestHandler { - public function __construct( - private RequestMatcher $inner, - ) { + ->mapRoute( + static fn(Component $route) => Component::of(static function( + ServerRequest $request, + mixed $input, + ) { + // use something stronger in a real app + if (!$request->headers()->contains('authorization')) { + return Attempt::error(new \RuntimeException('Missing authentication')); } - public function __invoke(ServerRequest $request): Response - { - // use something stronger in a real app - if (!$request->headers()->contains('authorization')) { - return Response::of( - StatusCode::unauthorized, - $request->protocolVersion(), - ); - } - - return ($this->inner)($request); - } - } + return Attempt::result($input); #(1) + })->pipe($route), ) ->service(/* ... */) ->service(/* ... */) - ->appendRoutes(/* ... */); + ->route(/* ... */) + ->route(/* ... */); } }; ``` -This example will refuse any request that doesn't have an `Authorization` header. Assuming you use a class instead of an anonymous one, you can disable a behaviour across your entire app by removing the one line calling `mapRequestHandler`. +1. You can replace `#!php $input` with the authenticated user, this variable will be carried to the next route component. -You can have multiple calls to `mapRequestHandler` to compose behaviours like an onion. +This example will refuse any request that doesn't have an `Authorization` header. Assuming you use a service instead of an inline component, you can disable a behaviour across your entire app by removing the one line calling `mapRoute`. -!!! note "" - The default request handler is the inner router of the framework, this means that you can completely change the default behaviour of the framework by returning a new request handler that never uses the default one. +You can have multiple calls to `mapRoute` to compose behaviours like an onion. ## Handling unknown routes @@ -347,17 +210,18 @@ use Innmind\Http\Message\{ Response\StatusCode, }; use Innmind\Filesystem\File\Content; +use Innmind\Immutable\Attempt; new class extends Http { protected function configure(Application $app): Application { - return $app->notFoundRequestHandler( - static fn(ServerRequest $request) => Response::of( + return $app->routeNotFound( + static fn(ServerRequest $request) => Attempt::result(Response::of( StatusCode::notFound, $request->protocolVersion(), null, Content::ofString('Page Not Found!'), //(1) - ), + )), ); } }; diff --git a/docs/middlewares.md b/docs/middlewares.md index 15f3331..1440180 100644 --- a/docs/middlewares.md +++ b/docs/middlewares.md @@ -1,6 +1,6 @@ # Middlewares -Middlewares are a way to regroup all the configuration you've seen in other topics under a name. This means that you can either group part of your own application under a middleware or expose a package for other to use via Packagist. +Middlewares are a way to regroup all the configuration you've seen in other sections under a name. This means that you can either group part of your own application under a middleware or expose a package for other to use via Packagist. !!! note "" You can search for [`innmind/framework-middlewares` on Packagist](https://packagist.org/providers/innmind/framework-middlewares) for middlewares published by others. @@ -19,8 +19,13 @@ use Innmind\DI\{ use Innmind\CLI\{ Console, Command, + Command\Usage, }; use Innmind\Url\Url; +use Innmind\Immutable\{ + Attempt, + Str, +}; enum Services implements Service { @@ -56,16 +61,16 @@ final class Emails implements Middleware ) { } - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { // send a test email here for example - return $console; + return $console->output(Str::of('Email sent')); } - public function usage(): string + public function usage(): Usage { - return 'email:test'; + return Usage::of('email:test'); } } ); diff --git a/docs/operating-system.md b/docs/operating-system.md index 3155f79..accba5c 100644 --- a/docs/operating-system.md +++ b/docs/operating-system.md @@ -1,6 +1,6 @@ # Decorate the operating system -The framework exposes an instance of [`OperatingSystem`](https://github.com/Innmind/OperatingSystem) in all configuration methods of `Application` offering you a wide range of abstractions. You can enhance its capabilities by adding a decorator on top of it. +The framework exposes an instance of [`OperatingSystem`](https://github.com/Innmind/OperatingSystem) in various methods of `Application` offering you a wide range of abstractions. You can enhance its capabilities by adding a decorator on top of it. For example `innmind/operating-system` comes with a decorator that use an exponential backoff strategy for the http client. @@ -12,14 +12,14 @@ use Innmind\Framework\{ }; use Innmind\OperatingSystem\{ OperatingSystem, - OperatingSystem\Resilient, + Config\Resilient, }; new class extends Http|Cli { protected function configure(Application $app): Application { return $app->mapOperatingSystem( - static fn(OperatingSystem $os) => Resilient::of($os), + static fn(OperatingSystem $os) => $os->map(Resilient::new()), ); } }; diff --git a/docs/services.md b/docs/services.md index d5be1a5..4be520d 100644 --- a/docs/services.md +++ b/docs/services.md @@ -58,19 +58,19 @@ use Innmind\Framework\{ }; use Innmind\OperatingSystem\OperatingSystem; use Innmind\AMQP\Factory; -use Innmind\Socket\Internet\Transport; -use Innmind\TimeContinuum\Earth\ElapsedPeriod; +use Innmind\IO\Sockets\Internet\Transport; +use Innmind\TimeContinuum\Period; use Innmind\Url\Url; new class extends Http|Cli { protected function configure(Application $app): Application { return $app->service( - Services::amqpClient, + Services::amqpClient(), static fn($_, OperatingSystem $os) => Factory::of($os)->make( Transport::tcp(), Url::of('amqp://guest:guest@localhost:5672/'), - ElapsedPeriod::of(1000), + Period::second(1), ), ); } @@ -95,15 +95,15 @@ use Innmind\Framework\{ }; use Innmind\OperatingSystem\OperatingSystem; use Innmind\AMQP\Factory; -use Innmind\Socket\Internet\Transport; -use Innmind\TimeContinuum\Earth\ElapsedPeriod; +use Innmind\IO\Sockets\Internet\Transport; +use Innmind\TimeContinuum\Period; use Innmind\Url\Url; new class extends Http|Cli { protected function configure(Application $app): Application { return $app->service( - Services::amqpClient, + Services::amqpClient(), static fn( $_, OperatingSystem $os, @@ -111,9 +111,9 @@ new class extends Http|Cli { ) => Factory::of($os)->make( Transport::tcp(), Url::of($env->get('AMQP_URL')), //(1) - ElapsedPeriod::of($env->maybe('AMQP_TIMEOUT')->match( //(2) + Period::second($env->maybe('AMQP_TIMEOUT')->match( //(2) static fn($timeout) => (int) $timeout, - static fn() => 1000, + static fn() => 1, )), ), ); @@ -122,7 +122,7 @@ new class extends Http|Cli { ``` 1. this will throw if the variable is not defined -2. in case the variable is not defined it will fallback to a `1000ms` timeout +2. in case the variable is not defined it will fallback to a `1s` timeout ## Services relying on services @@ -148,11 +148,11 @@ new class extends Http|Cli { { return $app ->service( - Services::producerClient, + Services::producerClient(), static fn($_, OperatingSystem $os) => Factory::of($os)->make(/* like above */), ) ->service( - Services::consumerClient, + Services::consumerClient(), static fn(Container $container) => $container(Services::producerClient)->with( Qos::of(10), // prefetch 10 messages ), diff --git a/docs/testing.md b/docs/testing.md index a42591c..18725b4 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -2,7 +2,7 @@ The best way to test your application is to move the whole configuration inside a [middleware](middlewares.md) that you can then reference in your tests. -If your whole is contained in a middleware called `Kernel` and you use PHPUnit your test could look like this: +If your whole app is contained in a middleware called `Kernel` and you use PHPUnit your test could look like this: ## For HTTP @@ -34,7 +34,7 @@ final class AppTest extends TestCase Url::of('/'), Method::get, ProtocolVersion::v20, - )); + ))->unwrap(); // $response is an instance of Response // write your assertions as usual @@ -71,7 +71,7 @@ final class AppTest extends TestCase ['entrypoint.php'], // arguments $variables, '/somewhere/', // working directory - )); + ))->unwrap(); $this->assertSame( [], @@ -89,15 +89,15 @@ final class AppTest extends TestCase Since we use a declarative approach and that `Application` is _immutable_ we can extend the behaviour of our app in our tests. -Say we want to write functional tests but we have an route that deletes data in a database but we can't verify the data is deleted through our routes. We can add a call to `mapRequestHandler` so we are in the context of our app and we can inject the test case to write our assertions. +Say we want to write functional tests but we have a route that deletes data in a database but we can't verify the data is deleted through our routes. We can add a call to `mapRoute` so we are in the context of our app and we can inject the test case to write our assertions. ```php use Innmind\Framework\{ Application, Environment, - Http\RequestHandler, }; use Innmind\OperatingSystem\Factory; +use Innmind\Router\Component; use Innmind\Http\{ ServerRequest, Response, @@ -105,6 +105,7 @@ use Innmind\Http\{ ProtocolVersion, }; use Innmind\Url\Url; +use Innmind\Immutable\Attempt; use PHPUnit\Framework\TestCase; final class AppTest extends TestCase @@ -116,37 +117,24 @@ final class AppTest extends TestCase 'AMQP_URL' => 'amqp://guest:guest@localhost:5672/', ])) ->map(new Kernel) - ->mapRequestHandler( - fn($handler, $container) => new class($handler, $container(Services::pdo), $this) implements RequestHandler { - public function __construct( - private RequestHandler $handler, - private \PDO $pdo, - private TestCase $test, - ) { - } - - public function __invoke(ServerRequest $request): Response - { - $response = ($this->handler)($request); - - $this->test->assertSame( - [], - $this - ->pdo - ->query('SELECT * FROM some_column WHERE condition_that_should_return_nothing') - ->fetchAll(), - ); - - return $response; - } - }, - ); + ->mapRoute(static fn($route, $container) => $route->pipe( + Component::of(function($request, $response) use ($container) { + $this->assertSame( + [], + $container(Services::pdo) + ->query('SELECT * FROM some_column WHERE condition_that_should_return_nothing') + ->fetchAll(), + ); + + return Attempt::result($response); + }), + )); $response = $app->run(ServerRequest::of( Url::of('/some-route/some-id'), Method::delete, ProtocolVersion::v20, - )); + ))->unwrap(); // $response is an instance of Response // write your assertions as usual