From 46f15bcc8f2a7be326acfd69117b89e7bbf01135 Mon Sep 17 00:00:00 2001
From: Mateusz Charytoniuk <mateusz.charytoniuk@protonmail.com>
Date: Tue, 27 Feb 2024 12:29:39 +0100
Subject: [PATCH] chore: update docs

---
 README.md                                     |  4 +-
 docs/pages/docs/changelog/index.md            |  2 +-
 .../docs/features/configuration/index.md      | 17 ++---
 .../features/database/doctrine/entities.md    | 17 +++--
 .../database/doctrine/entity-managers.md      | 37 ++++++++---
 .../features/graphql/developing-schema.md     |  8 +--
 docs/pages/docs/features/http/controllers.md  | 30 ++++-----
 docs/pages/docs/features/http/interceptors.md | 22 +++----
 docs/pages/docs/features/http/middleware.md   | 14 +++--
 .../docs/features/http/psr-http-messages.md   | 62 -------------------
 docs/pages/docs/features/http/responders.md   | 26 ++++----
 .../docs/features/http/serving-assets.md      |  6 +-
 docs/pages/docs/features/http/sessions.md     |  4 +-
 .../features/openapi/exposing-schema/index.md |  6 +-
 .../features/security/authentication/index.md | 14 ++---
 .../features/security/authorization/index.md  |  4 +-
 .../security/content-security-policy/index.md |  2 +-
 .../security/csrf-protection/index.md         | 26 ++++----
 .../oauth2/authorization-code-grant/index.md  |  6 +-
 .../security/oauth2/configuration/index.md    | 12 ++--
 .../templating/php-templates/index.md         | 44 ++++++-------
 .../templating/twig/rendering-templates.md    |  8 +--
 .../pages/docs/features/translations/index.md |  4 +-
 .../http-controller-parameters/index.md       | 14 ++---
 docs/pages/index.md                           |  7 ++-
 .../tutorials/basic-graphql-schema/index.md   |  6 +-
 docs/pages/tutorials/hello-world/index.md     | 14 ++---
 .../index.md                                  |  8 +--
 .../session-based-authentication/index.md     | 49 +++++++--------
 src/HttpInterceptor.php                       | 11 +++-
 .../ValidatesCSRFTokenMiddleware.php          |  3 +
 src/HttpResponder.php                         |  9 ++-
 src/HttpResponder/Error/ServerError.php       |  3 +-
 src/HttpResponder/Json.php                    |  3 +-
 src/HttpResponder/NotAcceptable.php           |  3 +-
 src/HttpResponderInterface.php                |  3 +-
 src/PsrStringStream.php                       |  8 ++-
 37 files changed, 246 insertions(+), 270 deletions(-)
 delete mode 100644 docs/pages/docs/features/http/psr-http-messages.md

diff --git a/README.md b/README.md
index 40bab801..69499896 100644
--- a/README.md
+++ b/README.md
@@ -94,9 +94,9 @@ advantage of the asynchronous environment.
 #[Singleton(collection: SingletonCollection::HttpResponder)]
 readonly class Homepage implements HttpResponderInterface
 {
-    public function respond(Request $request, Response $response): TwigTemplate
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): TwigTemplate
     {
-        return new TwigTemplate('website/homepage.twig');
+        return new TwigTemplate($request, $response, 'website/homepage.twig');
     }
 }
 ```
diff --git a/docs/pages/docs/changelog/index.md b/docs/pages/docs/changelog/index.md
index 69c6deaf..25423c3a 100644
--- a/docs/pages/docs/changelog/index.md
+++ b/docs/pages/docs/changelog/index.md
@@ -81,5 +81,5 @@ title: Changelog
 ## v0.10.0
 
 - Added {{docs/features/security/oauth2/index}} support.
-- Added {{docs/features/http/psr-http-messages}} wrapper.
+- Added PSR Http Messages wrapper.
 - Added `EntityManagerWeakReference` to {{docs/features/database/doctrine/index}} integration.
diff --git a/docs/pages/docs/features/configuration/index.md b/docs/pages/docs/features/configuration/index.md
index f76d7da9..54dbbd2a 100644
--- a/docs/pages/docs/features/configuration/index.md
+++ b/docs/pages/docs/features/configuration/index.md
@@ -283,8 +283,8 @@ use Distantmagic\Resonance\HttpResponder;
 use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -310,12 +310,15 @@ final readonly class Manifest extends HttpResponder
         ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
     }
 
-    public function respond(Request $request, Response $response): ?HttpResponderInterface
+    public function respond(
+        ServerRequestInterface $request, 
+        ResponseInterface $response,
+    ): ResponseInterface
     {
-        $response->header('content-type', ContentType::ApplicationJson->value);
-        $response->end($this->manifest);
-
-        return null;
+        return $response
+            ->withHeader('content-type', ContentType::ApplicationJson->value)
+            ->withBody($this->createStream($this->manifest))
+        ;
     }
 }
 ```
diff --git a/docs/pages/docs/features/database/doctrine/entities.md b/docs/pages/docs/features/database/doctrine/entities.md
index d927fb5e..ee6c758c 100644
--- a/docs/pages/docs/features/database/doctrine/entities.md
+++ b/docs/pages/docs/features/database/doctrine/entities.md
@@ -55,6 +55,8 @@ use Distantmagic\Resonance\HttpResponder\HttpController;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\TwigTemplate;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -64,7 +66,9 @@ use Distantmagic\Resonance\TwigTemplate;
 #[Singleton(collection: SingletonCollection::HttpResponder)]
 final readonly class BlogPostShow extends HttpController
 {
-    public function handle(
+    public function createResponse(
+        ServerRequestInterface $request,
+        ResponseInterface $response,
         #[DoctrineEntityRouteParameter(
             from: 'blog_post_slug', 
             intent: CrudAction::Read,
@@ -72,9 +76,14 @@ final readonly class BlogPostShow extends HttpController
         )]
         BlogPost $blogPost,
     ): HttpInterceptableInterface {
-        return new TwigTemplate('turbo/website/blog_post.twig', [
-            'blog_post' => $blogPost,
-        ]);
+        return new TwigTemplate(
+            $request, 
+            $response, 
+            'turbo/website/blog_post.twig', 
+            [
+                'blog_post' => $blogPost,
+            ],
+        );
     }
 }
 ```
diff --git a/docs/pages/docs/features/database/doctrine/entity-managers.md b/docs/pages/docs/features/database/doctrine/entity-managers.md
index 4e2ea6ce..f25c5c15 100644
--- a/docs/pages/docs/features/database/doctrine/entity-managers.md
+++ b/docs/pages/docs/features/database/doctrine/entity-managers.md
@@ -90,8 +90,8 @@ use Distantmagic\Resonance\HttpResponder;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\TwigTemplate;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -105,15 +105,23 @@ final readonly class Blog extends HttpResponder
         private DoctrineEntityManagerRepository $doctrineEntityManagerRepository,
     ) {}
 
-    public function respond(Request $request, Response $response): HttpInterceptableInterface
+    public function respond(
+        ServerRequestInterface $request, 
+        ResponseInterface $response,
+    ): HttpInterceptableInterface
     {
         $entityManager = $this->doctrineEntityManagerRepository->getEntityManager($request);
 
         $blogPostsRepository = $entityManager->getRepository(BlogPost::class);
 
-        return new TwigTemplate('turbo/website/blog.twig', [
-            'blog_posts' => $blogPostsRepository->findAll(),
-        ]);
+        return new TwigTemplate(
+            $request,
+            $response,
+            'turbo/website/blog.twig', 
+            [
+                'blog_posts' => $blogPostsRepository->findAll(),
+            ]
+        );
     }
 }
 ```
@@ -147,6 +155,8 @@ use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\TwigTemplate;
 use Doctrine\ORM\EntityManager;
 use Doctrine\ORM\EntityRepository;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -156,15 +166,22 @@ use Doctrine\ORM\EntityRepository;
 #[Singleton(collection: SingletonCollection::HttpResponder)]
 final readonly class Blog extends HttpController
 {
-    public function handle(
+    public function createResponse(
+        ServerRequestInterface $request,
+        ResponseInterface $response,
         #[DoctrineEntityManager]
         EntityManager $entityManager,
         #[DoctrineEntityRepository(BlogPost::class)]
         EntityRepository $blogPosts,
     ): HttpInterceptableInterface {
-        return new TwigTemplate('website/blog.twig', [
-            'blog_posts' => $blogPosts->findAll(),
-        ]);
+        return new TwigTemplate(
+            $request,
+            $response,
+            'website/blog.twig', 
+            [
+                'blog_posts' => $blogPosts->findAll(),
+            ]
+        );
     }
 }
 ```
diff --git a/docs/pages/docs/features/graphql/developing-schema.md b/docs/pages/docs/features/graphql/developing-schema.md
index 72c7faf8..33c5efdf 100644
--- a/docs/pages/docs/features/graphql/developing-schema.md
+++ b/docs/pages/docs/features/graphql/developing-schema.md
@@ -27,6 +27,7 @@ GraphQL handler.
 
 namespace App\HttpResponder;
 
+use App\HttpRouteSymbol;
 use Distantmagic\Resonance\Attribute\RespondsToHttp;
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\HttpResponder;
@@ -34,9 +35,8 @@ use Distantmagic\Resonance\HttpResponder\GraphQL as ResonanceGraphQL;
 use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
-use App\HttpRouteSymbol;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::POST,
@@ -48,7 +48,7 @@ final readonly class GraphQL extends HttpResponder
 {
     public function __construct(private ResonanceGraphQL $graphql) {}
 
-    public function respond(Request $request, Response $response): HttpResponderInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpResponderInterface
     {
         return $this->graphql;
     }
diff --git a/docs/pages/docs/features/http/controllers.md b/docs/pages/docs/features/http/controllers.md
index 34797557..e1d0d296 100644
--- a/docs/pages/docs/features/http/controllers.md
+++ b/docs/pages/docs/features/http/controllers.md
@@ -22,7 +22,8 @@ Controllers aim to automate these repetitive tasks as much as possible.
 ## Writing Controllers
 
 Unlike {{docs/features/http/responders}}, Controllers do not use the `respond` 
-method. Instead, they rely on the `handle` method to manage incoming requests. 
+method. Instead, they rely on the `createResponse` method to manage incoming 
+requests. 
 
 The `respond` method is used internally for handling tasks like parameter 
 binding, so you should not override it.
@@ -52,8 +53,8 @@ Using the `RouteParameter` might require to create a Crud Gate. See more at
 the {{docs/features/security/authorization/index}} page.
 :::
 
-Remember that the framework resolves parameters assigned to the `handle` method 
-on runtime during the request lifecycle.
+Remember that the framework resolves parameters assigned to the 
+`createResponse` method on runtime during the request lifecycle.
 
 The above is contrary to the constructor arguments, which the framework 
 resolves during the application bootstrap phase thanks to the 
@@ -78,7 +79,7 @@ use Distantmagic\Resonance\HttpResponder\HttpController;
 use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -88,13 +89,13 @@ use Swoole\Http\Response;
 #[Singleton(collection: SingletonCollection::HttpResponder)]
 final readonly class BlogPostShow extends HttpController
 {
-    public function handle(
+    public function createResponse(
         #[RouteParameter(
             from: 'blog_post_slug', 
             intent: CrudAction::Read,
         )]
         BlogPost $blogPost,
-        Response $response,
+        ResponseInterface $response,
     ): HttpResponderInterface {
         // ...
     }
@@ -142,8 +143,8 @@ final readonly class BlogPostBinder implements HttpRouteParameterBinderInterface
 ### Providing the Authenticated User (Session)
 
 If you need to fetch the authenticated user in your controller, you can add 
-a parameter with the `#[SessionAuthenticated]` attribute to the `handle` 
-method.
+a parameter with the `#[SessionAuthenticated]` attribute to the 
+`createResponse` method.
 
 The controller fetches an authenticated user through 
 {{docs/features/http/sessions}}.
@@ -162,7 +163,6 @@ use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\UserInterface;
-use Swoole\Http\Response;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -172,7 +172,7 @@ use Swoole\Http\Response;
 #[Singleton(collection: SingletonCollection::HttpResponder)]
 final readonly class MyController extends HttpController
 {
-    public function handle(
+    public function createResponse(
         // If you make this parameter required, then the framework will
         // return 403 page when user is unauthenticated.
         #[SessionAuthenticated]
@@ -201,8 +201,8 @@ use Distantmagic\Resonance\HttpControllerParameterResolution;
 use Distantmagic\Resonance\HttpControllerParameterResolutionStatus;
 use Distantmagic\Resonance\HttpControllerParameterResolver;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 /**
  * @template-extends HttpControllerParameterResolver<MyAttribute>
@@ -212,8 +212,8 @@ use Swoole\Http\Response;
 readonly class RouteParameterResolver extends HttpControllerParameterResolver
 {
     public function resolve(
-        Request $request,
-        Response $response,
+        ServerRequestInterface $request,
+        ResponseInterface $response,
         HttpControllerParameter $httpControllerParameter,
         Attribute $attribute,
     ): HttpControllerParameterResolution {
@@ -248,7 +248,7 @@ readonly class MyHttpController extends HttpController
         parent::__construct($httpControllerDependencies);
     }
 
-    public function handle()
+    public function createResponse()
     {
         // ...
     }
diff --git a/docs/pages/docs/features/http/interceptors.md b/docs/pages/docs/features/http/interceptors.md
index b85a39b4..3179659f 100644
--- a/docs/pages/docs/features/http/interceptors.md
+++ b/docs/pages/docs/features/http/interceptors.md
@@ -105,8 +105,8 @@ use Distantmagic\Resonance\HttpInterceptableInterface;
 use Distantmagic\Resonance\HttpInterceptor;
 use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 /**
  * @template-extends HttpInterceptor<Hello>
@@ -116,14 +116,14 @@ use Swoole\Http\Response;
 readonly class HelloInterceptor extends HttpInterceptor
 {
     public function intercept(
-        Request $request,
-        Response $response,
+        ServerRequestInterface $request,
+        ResponseInterface $response,
         object $intercepted,
     ): HttpInterceptableInterface|HttpResponderInterface {
-        $response->header('content-type', 'text/plain');
-        $response->end('Hello, '.$intercepted->message.'!');
-
-        return null;
+        return $response
+            ->withHeader('content-type', 'text/plain')
+            ->withBody($this->createStream('Hello, '.$intercepted->message.'!'))
+        ;
     }
 }
 ```
@@ -143,8 +143,8 @@ use Distantmagic\Resonance\HttpInterceptableInterface;
 use Distantmagic\Resonance\HttpResponder;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -154,7 +154,7 @@ use Swoole\Http\Response;
 #[Singleton(collection: SingletonCollection::HttpResponder)]
 final readonly class HelloResponder extends HttpResponder
 {
-    public function respond(Request $request, Response $response): HttpInterceptableInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpInterceptableInterface
     {
         return new Hello('World');
     }
diff --git a/docs/pages/docs/features/http/middleware.md b/docs/pages/docs/features/http/middleware.md
index 8fbbe351..b2845ded 100644
--- a/docs/pages/docs/features/http/middleware.md
+++ b/docs/pages/docs/features/http/middleware.md
@@ -92,13 +92,15 @@ use Distantmagic\Resonance\Attribute\ValidatesCSRFToken;
 #[ValidatesCSRFToken]
 final readonly class BlogPostDestroy extends HttpController
 {
-    public function handle(
+    public function createResponse(
+        ServerRequestInterface $request,
+        ResponseInterface $response,
         #[RouteParameter(from: 'blog_post_slug', intent: CrudAction::Delete)]
         BlogPost $blogPost,
     ): HttpInterceptableInterface {
         // ...
 
-        return new InternalRedirect(HttpRouteSymbol::Blog);
+        return new InternalRedirect($request, $response, HttpRouteSymbol::Blog);
     }
 }
 
@@ -161,8 +163,8 @@ use Distantmagic\Resonance\HttpInterceptableInterface;
 use Distantmagic\Resonance\HttpMiddleware;
 use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 /**
  * @template-extends HttpMiddleware<MyAttribute>
@@ -175,8 +177,8 @@ use Swoole\Http\Response;
 readonly class CanMiddleware extends HttpMiddleware
 {
     public function preprocess(
-        Request $request,
-        Response $response,
+        ServerRequestInterface $request,
+        ResponseInterface $response,
         Attribute $attribute,
         HttpInterceptableInterface|HttpResponderInterface $next,
     ): HttpInterceptableInterface|HttpResponderInterface {
diff --git a/docs/pages/docs/features/http/psr-http-messages.md b/docs/pages/docs/features/http/psr-http-messages.md
deleted file mode 100644
index ed4d9f79..00000000
--- a/docs/pages/docs/features/http/psr-http-messages.md
+++ /dev/null
@@ -1,62 +0,0 @@
----
-collections: 
-    - documents
-layout: dm:document
-parent: docs/features/http/index
-title: PSR HTTP Messages
-description: >
-    Learn how to convert Swoole server requests to PSR server requests.
----
-
-# PSR HTTP Messages
-
-If you need to convert Swoole's HTTP Request object to it's 
-[PSR counterpart](https://www.php-fig.org/psr/psr-7/) you can use the 
-converters.
-
-# Usage
-
-## Swoole Request -> PSR Server Request
-
-You should only convert requests if you need to use some third-party library 
-that relies on them. Primarily because PSR requests do not provide any 
-additional features, it's just for standardization. Conversion between request
-formats hinders the performance.
-
-`PsrServerRequestConverter` can/should also be used as a singleton. 
-
-```php
-/**
- * @var Distantmagic\Resonance\PsrServerRequestConverter $psrServerRequestRepository 
- * @var Swoole\Http\Request $request 
- * @var Psr\Http\Message\ServerRequestInterface $psrRequest
- */
-$psrRequest = $psrServerRequestRepository->convertToServerRequest($request);
-```
-
-## PSR Response -> Swoole Response
-
-If you want to respond with PSR response, you need to wrap it in 
-`PsrResponder`:
-
-```php
-use Distantmagic\Resonance\HttpResponder;
-use Distantmagic\Resonance\HttpResponderInterface;
-use Distantmagic\Resonance\HttpResponder\PsrResponder;
-use Psr\Http\Message\ResponseInterface;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
-
-readonly class MyResponder extends HttpResponder
-{
-    public function respond(Request $request, Response $response): HttpResponderInterface
-    {
-        // (...) obtain psr response somehow
-
-        /**
-         * @var ResponseInterface $psrResponse
-         */
-        return new PsrResponder($psrResponse);
-    }
-}
-```
diff --git a/docs/pages/docs/features/http/responders.md b/docs/pages/docs/features/http/responders.md
index d82305c9..81c6cda2 100644
--- a/docs/pages/docs/features/http/responders.md
+++ b/docs/pages/docs/features/http/responders.md
@@ -58,8 +58,8 @@ use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -69,11 +69,9 @@ use Swoole\Http\Response;
 #[Singleton(collection: SingletonCollection::HttpResponder)]
 final readonly class Homepage implements HttpResponderInterface
 {
-    public function respond(Request $request, Response $response): ?HttpResponderInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
     {
-        $response->end('Hello, world!');
-
-        return null;
+        return $response->withBody($this->createStream('Hello, world!'));
     }
 }
 ```
@@ -90,14 +88,14 @@ For example:
 
 use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\HttpResponder\Redirect;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 class MyResponder implements HttpResponderInterface
 {
     // (...)
 
-    public function respond(Request $request, Response $response): HttpResponderInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpResponderInterface
     {
         return new Redirect('/blog');
     }
@@ -110,18 +108,16 @@ You can even use anonymous classes:
 <?php
 
 use Distantmagic\Resonance\HttpResponderInterface;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 class MyResponder implements HttpResponderInterface
 {
     public function respond(Request $request, Response $response): ?HttpResponderInterface
     {
         return new class implements HttpResponderInterface {
-            public function respond (Request $request, Response $response): null {
-                $response->end('Hello!');
-
-                return null;
+            public function respond (ServerRequestInterface $request, ResponseInterface $response): ResponseInterface {
+                return $response->withBody($this->createStream('Hello!'));
             }
         };
     }
diff --git a/docs/pages/docs/features/http/serving-assets.md b/docs/pages/docs/features/http/serving-assets.md
index 00b45711..b62649dd 100644
--- a/docs/pages/docs/features/http/serving-assets.md
+++ b/docs/pages/docs/features/http/serving-assets.md
@@ -47,8 +47,8 @@ use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\HttpRouteMatchRegistry;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -66,7 +66,7 @@ final readonly class Asset extends HttpResponder
         private HttpRouteMatchRegistry $routeMatchRegistry,
     ) {}
 
-    public function respond(Request $request, Response $response): ?HttpResponderInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpResponderInterface|ResponseInterface
     {
         return $this->assetFileRegistry->sendAsset(
             $response, 
diff --git a/docs/pages/docs/features/http/sessions.md b/docs/pages/docs/features/http/sessions.md
index 7c42d17f..9fe4a773 100644
--- a/docs/pages/docs/features/http/sessions.md
+++ b/docs/pages/docs/features/http/sessions.md
@@ -77,10 +77,10 @@ The typical usage follows the 'start -> modify -> persist' pattern:
  * `$sessionManager->start()` also sets the session cookie in the response.
  * 
  * @var Resonance\Session $session
- * @var Swoole\Http\Request $request
+ * @var Psr\Http\Message\ServerRequestInterface $request
  * @var Swoole\Http\Response $response
  */
-$session = $sessionManager->start($request, $response);
+$session = $sessionManager->start($request);
 
 /**
  * Anything that is serializable by igbinary can be used as a value.
diff --git a/docs/pages/docs/features/openapi/exposing-schema/index.md b/docs/pages/docs/features/openapi/exposing-schema/index.md
index fb121c00..11b933b9 100644
--- a/docs/pages/docs/features/openapi/exposing-schema/index.md
+++ b/docs/pages/docs/features/openapi/exposing-schema/index.md
@@ -34,8 +34,8 @@ use Distantmagic\Resonance\OpenAPISchemaBuilder;
 use Distantmagic\Resonance\OpenAPISchemaSymbol;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -51,7 +51,7 @@ readonly class OpenAPISchema implements HttpResponderInterface
         $this->schema = $openAPISchemaBuilder->toJsonResponse(OpenAPISchemaSymbol::All);
     }
 
-    public function respond(Request $request, Response $response): HttpResponderInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpResponderInterface
     {
         return $this->schema;
     }
diff --git a/docs/pages/docs/features/security/authentication/index.md b/docs/pages/docs/features/security/authentication/index.md
index 72a9eb23..a8a0e053 100644
--- a/docs/pages/docs/features/security/authentication/index.md
+++ b/docs/pages/docs/features/security/authentication/index.md
@@ -56,7 +56,7 @@ final readonly class LoginValidation extends HttpController
         parent::__construct($controllerDependencies);
     }
 
-    public function handle(Request $request, Response $response): HttpInterceptableInterface {
+    public function createResponse(ServerRequestInterface $request, ResponseInterface $response): HttpInterceptableInterface {
         $user = /* obtain user somehow */;
 
         $this->sessionAuthentication->setAuthenticatedUser(
@@ -65,7 +65,7 @@ final readonly class LoginValidation extends HttpController
             $user,
         );
 
-        return new InternalRedirect(HttpRouteSymbol::Homepage);
+        return new InternalRedirect($request, $response, HttpRouteSymbol::Homepage);
     }
 }
 ```
@@ -112,7 +112,7 @@ final readonly class MyController extends HttpController
     /**
      * @param ?UserInterface $user null if not authenticated
      */
-    public function handle(
+    public function createResponse(
         Request $request,
         #[SessionAuthenticated]
         ?UserInterface $user,
@@ -133,13 +133,13 @@ use Distantmagic\Resonance\Attribute\ProvidesAuthenticatedUser;
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\AuthenticatedUserStoreInterface;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Request;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[ProvidesAuthenticatedUser(1200)]
 #[Singleton(collection: SingletonCollection::AuthenticatedUserStore)]
 readonly class MyAuthentication implements AuthenticatedUserStoreInterface
 {
-    public function getAuthenticatedUser(Request $request): ?AuthenticatedUser
+    public function getAuthenticatedUser(ServerRequestInterface $request): ?AuthenticatedUser
     {
         // ...
     }
@@ -158,7 +158,7 @@ use Distantmagic\Resonance\AuthenticatedUserStoreInterface;
 use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\UserInterface;
 use League\OAuth2\Client\Provider\GenericProvider;
-use Swoole\Http\Request;
+use Psr\Http\Message\ServerRequestInterface;
 
 use function Swoole\Coroutine\Http\get;
 
@@ -166,7 +166,7 @@ use function Swoole\Coroutine\Http\get;
 #[Singleton(collection: SingletonCollection::AuthenticatedUserStore)]
 readonly class MyAuthentication implements AuthenticatedUserStoreInterface
 {
-    public function getAuthenticatedUser(Request $request): ?AuthenticatedUser
+    public function getAuthenticatedUser(ServerRequestInterface $request): ?AuthenticatedUser
     {
         $userData = get('https://your-server.example.com/user', [], [
             'Authorization' => sprintf('Bearer %s', $request->cookie['access_token']),
diff --git a/docs/pages/docs/features/security/authorization/index.md b/docs/pages/docs/features/security/authorization/index.md
index 38ee0fbc..09fab519 100644
--- a/docs/pages/docs/features/security/authorization/index.md
+++ b/docs/pages/docs/features/security/authorization/index.md
@@ -105,14 +105,14 @@ You can use `Gatekeeper` in your code to check permissions. For example:
 use App\BlogPostInterface;
 use Distantmagic\Resonance\CrudAction;
 use Distantmagic\Resonance\Gatekeeper;
-use Swoole\Http\Request;
+use Psr\Http\Message\ServerRequestInterface;
 
 readonly class MyClass
 {
     public function __construct(private Gatekeeper $gatekeeper) {}
 
     public function showBlogPost(
-        Request $request,
+        ServerRequestInterface $request,
         BlogPostInterface $blogPost,
     ): string
     {
diff --git a/docs/pages/docs/features/security/content-security-policy/index.md b/docs/pages/docs/features/security/content-security-policy/index.md
index a9729dbf..85e775a6 100644
--- a/docs/pages/docs/features/security/content-security-policy/index.md
+++ b/docs/pages/docs/features/security/content-security-policy/index.md
@@ -59,7 +59,7 @@ To use nonces manually, you need to use the CSP Nonce Manager:
 ```php
 /**
  * @var \Distantmagic\Resonance\CSPNonceManager $cspNonceManager
- * @var \Swoole\Http\Request $request
+ * @var \Psr\Http\Message\ServerRequestInterface $request
  */
 $cspNonceManager->getRequestNonce($request);
 ```
diff --git a/docs/pages/docs/features/security/csrf-protection/index.md b/docs/pages/docs/features/security/csrf-protection/index.md
index 393de177..83271a07 100644
--- a/docs/pages/docs/features/security/csrf-protection/index.md
+++ b/docs/pages/docs/features/security/csrf-protection/index.md
@@ -103,14 +103,14 @@ response and the `respond` method will not be called.
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\Attribute\ValidatesCSRFToken;
 use Distantmagic\Resonance\HttpResponderInterface;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[Singleton]
 #[ValidatesCSRFToken]
 final readonly class MyResponder implements HttpResponderInterface
 {
-    public function respond(Request $request, Response $response): void 
+    public function respond(ServerRequestInterface $request, ResponseInterfaced $response): void 
     {
         // CSRF token is valid...
     }
@@ -124,24 +124,24 @@ final readonly class MyResponder implements HttpResponderInterface
 
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\CSRFManager;
-use Distantmagic\Resonance\HttpResponderInterface;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Distantmagic\Resonance\HttpResponder;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[Singleton]
-final readonly class MyResponder implements HttpResponderInterface
+final readonly class MyResponder extends HttpResponder
 {
     public function __construct(private CSRFManager $csrfManager) 
     {
     }
 
-    public function respond(Request $request, Response $response): void 
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
     {
-        if (!$this->csrfManager->checkToken($request, $request->post)) {
-            $response->status(400);
-            $response->end('Bad Request: CSRF Token is invalid.');
-
-            return;
+        if (!$this->csrfManager->checkToken($request, $request->getParsedBody())) {
+            return $response
+                ->withStatus(400)
+                ->withBody($this->createStream('Bad Request: CSRF Token is invalid.'))
+            ;
         }
 
         // CSRF token is valid...
diff --git a/docs/pages/docs/features/security/oauth2/authorization-code-grant/index.md b/docs/pages/docs/features/security/oauth2/authorization-code-grant/index.md
index 8d98bd94..4a22204c 100644
--- a/docs/pages/docs/features/security/oauth2/authorization-code-grant/index.md
+++ b/docs/pages/docs/features/security/oauth2/authorization-code-grant/index.md
@@ -85,8 +85,8 @@ use Distantmagic\Resonance\HttpResponder\OAuth2\Authorization;
 use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::POST,
@@ -98,7 +98,7 @@ final readonly class OAuth2AuthorizationServer extends HttpResponder
 {
     public function __construct(private Authorization $authorizationServer) {}
 
-    public function respond(Request $request, Response $response): HttpResponderInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpResponderInterface
     {
         return $this->authorizationServer;
     }
diff --git a/docs/pages/docs/features/security/oauth2/configuration/index.md b/docs/pages/docs/features/security/oauth2/configuration/index.md
index 52f5f615..b9d70000 100644
--- a/docs/pages/docs/features/security/oauth2/configuration/index.md
+++ b/docs/pages/docs/features/security/oauth2/configuration/index.md
@@ -51,8 +51,8 @@ use Distantmagic\Resonance\HttpResponder\OAuth2\AccessToken;
 use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::POST,
@@ -64,7 +64,7 @@ final readonly class OAuth2AccessToken extends HttpResponder
 {
     public function __construct(private AccessToken $accessTokenResponder) {}
 
-    public function respond(Request $request, Response $response): HttpResponderInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpResponderInterface
     {
         return $this->accessTokenResponder;
     }
@@ -88,8 +88,8 @@ use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\PsrServerRequestConverter;
 use League\OAuth2\Server\AuthorizationServer as LeagueAuthorizationServer;
 use Nyholm\Psr7\Factory\Psr17Factory;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[Singleton]
 readonly class MyOAuth2Server extends HttpResponder
@@ -100,7 +100,7 @@ readonly class MyOAuth2Server extends HttpResponder
         private Psr17Factory $psr17Factory,
     ) {}
 
-    public function respond(Request $request, Response $response): HttpResponderInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpResponderInterface
     {
         /**
          * Convert Swoole http request to PSR Server request object
diff --git a/docs/pages/docs/features/templating/php-templates/index.md b/docs/pages/docs/features/templating/php-templates/index.md
index 6ea8cc66..7762a3b6 100644
--- a/docs/pages/docs/features/templating/php-templates/index.md
+++ b/docs/pages/docs/features/templating/php-templates/index.md
@@ -39,11 +39,11 @@ Template file:
 ```php
 <?php
 
-use Distantmagic\Resonance\HttpResponderInterface;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Distantmagic\Resonance\HttpResponder;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
-readonly class MyBlogPostTemplate implements HttpResponderInterface
+readonly class MyBlogPostTemplate extends HttpResponder
 {
     public function __construct(
         private string $title, 
@@ -52,9 +52,9 @@ readonly class MyBlogPostTemplate implements HttpResponderInterface
     {
     }
 
-    public function respond(Request $request, Response $response): null
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
     {
-        $response->end(<<<HTML
+        return $response->with($this->createStream(<<<HTML
         <html>
             <head></head>
             <body>
@@ -62,9 +62,7 @@ readonly class MyBlogPostTemplate implements HttpResponderInterface
                 <p>{$this->content}</p>
             </body>
         </html>
-        HTML);
-
-        return null;
+        HTML));
     }
 }
 ```
@@ -75,12 +73,12 @@ Responder:
 <?php
 
 use Distantmagic\Resonance\HttpResponderInterface;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 readonly class MyResponder implements HttpResponderInterface
 {
-    public function respond(Request $request, Response $response): HttpResponderInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpResponderInterface
     {
         return new MyTemplate('title', 'content');
     }
@@ -103,17 +101,17 @@ kind of performance.
 ```php
 <?php
 
-use Distantmagic\Resonance\HttpResponderInterface;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Distantmagic\Resonance\HttpResponder;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
-abstract readonly class MyBaseTemplate implements HttpResponderInterface
+abstract readonly class MyBaseTemplate extends HttpResponder
 {
     abstract protected function renderBodyContent(Request $request, Response $response): string;
 
-    public function respond(Request $request, Response $response): null
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
     {
-        $response->end(<<<HTML
+        return $response->withBody($this->createStream(<<<HTML
         <html>
             <head></head>
             <body>
@@ -121,9 +119,7 @@ abstract readonly class MyBaseTemplate implements HttpResponderInterface
                 {$this->renderBodyContent($request, $response)}
             </body>
         </html>
-        HTML);
-
-        return null;
+        HTML));
     }
 }
 ```
@@ -133,12 +129,12 @@ Then in other pages:
 ```php
 <?php
 
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 readonly class MyBlogPost extends MyBaseTemplate
 {
-    protected function renderBodyContent(Request $request, Response $response): string
+    protected function renderBodyContent(ServerRequestInterface $request, ResponseInterface $response): string
     {
         return 'Hello!';
     }
diff --git a/docs/pages/docs/features/templating/twig/rendering-templates.md b/docs/pages/docs/features/templating/twig/rendering-templates.md
index 93d262f3..ab42b7fc 100644
--- a/docs/pages/docs/features/templating/twig/rendering-templates.md
+++ b/docs/pages/docs/features/templating/twig/rendering-templates.md
@@ -27,8 +27,8 @@ use Distantmagic\Resonance\HttpInterceptableInterface;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\TwigTemplate;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -38,9 +38,9 @@ use Swoole\Http\Response;
 #[Singleton(collection: SingletonCollection::HttpResponder)]
 final readonly class Twig extends HttpResponder
 {
-    public function respond(Request $request, Response $response): HttpInterceptableInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpInterceptableInterface
     {
-        return new TwigTemplate('test.twig');
+        return new TwigTemplate($request, $response, 'test.twig');
     }
 }
 ```
diff --git a/docs/pages/docs/features/translations/index.md b/docs/pages/docs/features/translations/index.md
index 8040af1a..1e9dbe9c 100644
--- a/docs/pages/docs/features/translations/index.md
+++ b/docs/pages/docs/features/translations/index.md
@@ -50,7 +50,7 @@ header).
 
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\TranslatorBridge;
-use Swoole\Http\Request;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[Singleton]
 readonly class MyClass
@@ -59,7 +59,7 @@ readonly class MyClass
     {
     }
 
-    public function doSomething(Request $request): string
+    public function doSomething(ServerRequestInterface $request): string
     {
         return $this->translator->trans(
             $request, 
diff --git a/docs/pages/docs/features/validation/http-controller-parameters/index.md b/docs/pages/docs/features/validation/http-controller-parameters/index.md
index ab9c1def..11f7a073 100644
--- a/docs/pages/docs/features/validation/http-controller-parameters/index.md
+++ b/docs/pages/docs/features/validation/http-controller-parameters/index.md
@@ -33,7 +33,7 @@ going to inject the data model into the parameter:
 ```php
 // ...
 
-public function handle(
+public function createResponse(
     #[ValidatedRequest(MyValidator::class)]
     MyValidatedData $data,
 ) {
@@ -82,8 +82,8 @@ use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
 use Ds\Map;
 use Ds\Set;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::POST,
@@ -93,7 +93,7 @@ use Swoole\Http\Response;
 #[Singleton(collection: SingletonCollection::HttpResponder)]
 final readonly class BlogPostStore extends HttpController
 {
-    public function handle(
+    public function createResponse(
         #[ValidatedRequest(BlogPostFormValidator::class)]
         #[OnParameterResolution(
             status: HttpControllerParameterResolutionStatus::ValidationErrors,
@@ -109,11 +109,11 @@ final readonly class BlogPostStore extends HttpController
      * @param Map<string,Set<string>> $errors
      */
     public function handleValidationErrors(
-        Request $request,
-        Response $response,
+        ServerRequestInterface $request,
+        ResponseInterface $response,
         HttpControllerParameterResolution $resolution,
     ): HttpResponderInterface {
-        $response->status(400);
+        $response = $response->withStatus(400);
 
         /* render form with errors  */
         /* ... */
diff --git a/docs/pages/index.md b/docs/pages/index.md
index cd0fc5e2..f532ee2d 100644
--- a/docs/pages/index.md
+++ b/docs/pages/index.md
@@ -202,9 +202,12 @@ final readonly class EchoResponder extends WebSocketRPCResponder
 #[Singleton(collection: SingletonCollection::HttpResponder)]
 readonly class Homepage implements HttpResponderInterface
 {
-    public function respond(Request $request, Response $response): TwigTemplate
+    public function respond(
+        ServerRequestInterface $request, 
+        ResponseInterface $response
+    ): TwigTemplate
     {
-        return new TwigTemplate('website/homepage.twig');
+        return new TwigTemplate($request, $response, 'website/homepage.twig');
     }
 }</code></pre>
             </li>
diff --git a/docs/pages/tutorials/basic-graphql-schema/index.md b/docs/pages/tutorials/basic-graphql-schema/index.md
index 71c9cfbf..d18508c0 100644
--- a/docs/pages/tutorials/basic-graphql-schema/index.md
+++ b/docs/pages/tutorials/basic-graphql-schema/index.md
@@ -73,8 +73,8 @@ use Distantmagic\Resonance\HttpResponderInterface;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\SiteAction;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::POST,
@@ -86,7 +86,7 @@ final readonly class GraphQL extends HttpResponder
 {
     public function __construct(private ResonanceGraphQL $graphql) {}
 
-    public function respond(Request $request, Response $response): HttpResponderInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpResponderInterface
     {
         return $this->graphql;
     }
diff --git a/docs/pages/tutorials/hello-world/index.md b/docs/pages/tutorials/hello-world/index.md
index 696d334e..ed468486 100644
--- a/docs/pages/tutorials/hello-world/index.md
+++ b/docs/pages/tutorials/hello-world/index.md
@@ -102,8 +102,8 @@ use Distantmagic\Resonance\HttpResponder;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\TwigTemplate;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -113,9 +113,9 @@ use Swoole\Http\Response;
 #[Singleton(collection: SingletonCollection::HttpResponder)]
 final readonly class Homepage extends HttpResponder
 {
-    public function respond(Request $request, Response $response): HttpInterceptableInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpInterceptableInterface
     {
-        return new TwigTemplate('homepage.twig');
+        return new TwigTemplate($request, $response, 'homepage.twig');
     }
 }
 ```
@@ -190,8 +190,8 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance;
 
 use Distantmagic\Resonance\Attribute\ContentSecurityPolicy;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[ContentSecurityPolicy(ContentSecurityPolicyType::Html)]
 final readonly class TwigTemplate implements HttpInterceptableInterface
@@ -201,7 +201,7 @@ final readonly class TwigTemplate implements HttpInterceptableInterface
         private array $templateData = [],
     ) {}
 
-    public function getTemplateData(Request $request, Response $response): array
+    public function getTemplateData(ServerRequestInterface $request, ResponseInterface $response): array
     {
         return $this->templateData + [
             'request' => $request,
diff --git a/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md b/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md
index db9fe2b2..811698d3 100644
--- a/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md
+++ b/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md
@@ -98,8 +98,8 @@ use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\SiteAction;
 use Distantmagic\Resonance\TwigTemplate;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[Can(SiteAction::StartWebSocketRPCConnection)]
 #[RespondsToHttp(
@@ -111,9 +111,9 @@ use Swoole\Http\Response;
 #[WantsFeature(Feature::WebSocket)]
 final readonly class LlmChat extends HttpResponder
 {
-    public function respond(Request $request, Response $response): HttpInterceptableInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpInterceptableInterface
     {
-        return new TwigTemplate('turbo/llmchat/index.twig');
+        return new TwigTemplate($request, $response, 'turbo/llmchat/index.twig');
     }
 }
 ```
diff --git a/docs/pages/tutorials/session-based-authentication/index.md b/docs/pages/tutorials/session-based-authentication/index.md
index 0b1f188a..7a127aba 100644
--- a/docs/pages/tutorials/session-based-authentication/index.md
+++ b/docs/pages/tutorials/session-based-authentication/index.md
@@ -159,7 +159,6 @@ use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\DoctrineEntityManagerRepository;
 use Distantmagic\Resonance\UserInterface;
 use Distantmagic\Resonance\UserRepositoryInterface;
-use Swoole\Http\Request;
 
 #[Singleton(provides: UserRepositoryInterface::class)]
 readonly class UserRepository implements UserRepositoryInterface
@@ -243,8 +242,8 @@ use Distantmagic\Resonance\HttpResponder;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\TwigTemplate;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -254,9 +253,9 @@ use Swoole\Http\Response;
 #[Singleton(collection: SingletonCollection::HttpResponder)]
 final readonly class LoginForm extends HttpResponder
 {
-    public function respond(Request $request, Response $response): HttpInterceptableInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpInterceptableInterface
     {
-        return new TwigTemplate('auth/login_form.twig');
+        return new TwigTemplate($request, $response, 'auth/login_form.twig');
     }
 }
 ```
@@ -430,8 +429,8 @@ use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\TwigTemplate;
 use Doctrine\ORM\EntityRepository;
 use Ds\Map;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::POST,
@@ -449,14 +448,14 @@ final readonly class LoginValidation extends HttpController
         parent::__construct($controllerDependencies);
     }
 
-    public function handle(
-        Request $request,
-        Response $response,
+    public function createResponse(
+        ServerRequestInterface $request,
+        ResponseInterface $response,
         #[ValidatedRequest(UsernamePasswordValidator::class)]
         UsernamePassword $usernamePassword,
         #[DoctrineEntityRepository(User::class)]
         EntityRepository $users,
-    ): null|HttpInterceptableInterface {
+    ): HttpInterceptableInterface|ResponseInterface {
         /**
          * @var null|User
          */
@@ -465,9 +464,7 @@ final readonly class LoginValidation extends HttpController
         ]);
 
         if (!$user || !password_verify($usernamePassword->password, $user->getPasswordHash())) {
-            $response->status(403);
-
-            return;
+            return $response->withStatus(403);
         }
 
         $this->sessionAuthentication->setAuthenticatedUser(
@@ -476,7 +473,7 @@ final readonly class LoginValidation extends HttpController
             $user->user,
         );
 
-        return new InternalRedirect(HttpRouteSymbol::Homepage);
+        return new InternalRedirect($request, $response, HttpRouteSymbol::Homepage);
     }
 }
 ```
@@ -502,8 +499,8 @@ use Distantmagic\Resonance\HttpResponder;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\TwigTemplate;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::GET,
@@ -513,9 +510,9 @@ use Swoole\Http\Response;
 #[Singleton(collection: SingletonCollection::HttpResponder)]
 final readonly class LogoutForm extends HttpResponder
 {
-    public function respond(Request $request, Response $response): HttpInterceptableInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpInterceptableInterface
     {
-        return new TwigTemplate('auth/logout_form.twig');
+        return new TwigTemplate($request, $response, 'auth/logout_form.twig');
     }
 }
 ```
@@ -552,8 +549,8 @@ use Distantmagic\Resonance\InternalRedirect;
 use Distantmagic\Resonance\RequestMethod;
 use Distantmagic\Resonance\SessionAuthentication;
 use Distantmagic\Resonance\SingletonCollection;
-use Swoole\Http\Request;
-use Swoole\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 #[RespondsToHttp(
     method: RequestMethod::POST,
@@ -568,13 +565,15 @@ final readonly class LogoutValidation extends HttpResponder
         private SessionAuthentication $sessionAuthentication,
     ) {}
 
-    public function respond(Request $request, Response $response): HttpInterceptableInterface
+    public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpInterceptableInterface
     {
         $this->sessionAuthentication->clearAuthenticatedUser($request);
 
-        $response->header('clear-site-data', '*');
-
-        return new InternalRedirect(HttpRouteSymbol::Homepage);
+        return new InternalRedirect(
+            $request,
+            $response->header('clear-site-data', '*');
+            HttpRouteSymbol::Homepage,
+        );
     }
 }
 ```
diff --git a/src/HttpInterceptor.php b/src/HttpInterceptor.php
index 25fa9d2d..d0aad021 100644
--- a/src/HttpInterceptor.php
+++ b/src/HttpInterceptor.php
@@ -4,9 +4,18 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
+use Psr\Http\Message\StreamInterface;
+use Stringable;
+
 /**
  * @template TClass
  *
  * @template-implements HttpInterceptorInterface<TClass>
  */
-abstract readonly class HttpInterceptor implements HttpInterceptorInterface {}
+abstract readonly class HttpInterceptor implements HttpInterceptorInterface
+{
+    public function createStream(string|Stringable $contents): StreamInterface
+    {
+        return new PsrStringStream($contents);
+    }
+}
diff --git a/src/HttpMiddleware/ValidatesCSRFTokenMiddleware.php b/src/HttpMiddleware/ValidatesCSRFTokenMiddleware.php
index 0e853a8e..993c4052 100644
--- a/src/HttpMiddleware/ValidatesCSRFTokenMiddleware.php
+++ b/src/HttpMiddleware/ValidatesCSRFTokenMiddleware.php
@@ -5,10 +5,12 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\HttpMiddleware;
 
 use Distantmagic\Resonance\Attribute;
+use Distantmagic\Resonance\Attribute\GrantsFeature;
 use Distantmagic\Resonance\Attribute\HandlesMiddlewareAttribute;
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\Attribute\ValidatesCSRFToken;
 use Distantmagic\Resonance\CSRFManager;
+use Distantmagic\Resonance\Feature;
 use Distantmagic\Resonance\HttpInterceptableInterface;
 use Distantmagic\Resonance\HttpMiddleware;
 use Distantmagic\Resonance\HttpResponder\Error\BadRequest;
@@ -21,6 +23,7 @@ use Psr\Http\Message\ServerRequestInterface;
 /**
  * @template-extends HttpMiddleware<ValidatesCSRFToken>
  */
+#[GrantsFeature(Feature::HttpSession)]
 #[HandlesMiddlewareAttribute(
     attribute: ValidatesCSRFToken::class,
     priority: 1100,
diff --git a/src/HttpResponder.php b/src/HttpResponder.php
index 405288b2..85b40c6d 100644
--- a/src/HttpResponder.php
+++ b/src/HttpResponder.php
@@ -4,14 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
-use LogicException;
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\StreamInterface;
+use Stringable;
 
 abstract readonly class HttpResponder implements HttpResponderInterface
 {
-    public function handle(ServerRequestInterface $request): ResponseInterface
+    public function createStream(string|Stringable $contents): StreamInterface
     {
-        throw new LogicException('This method should not be called');
+        return new PsrStringStream($contents);
     }
 }
diff --git a/src/HttpResponder/Error/ServerError.php b/src/HttpResponder/Error/ServerError.php
index 171ddb5b..7473ca60 100644
--- a/src/HttpResponder/Error/ServerError.php
+++ b/src/HttpResponder/Error/ServerError.php
@@ -13,7 +13,6 @@ use Distantmagic\Resonance\HttpError\ServerError as ServerErrorEntity;
 use Distantmagic\Resonance\HttpInterceptableInterface;
 use Distantmagic\Resonance\HttpResponder\Error;
 use Distantmagic\Resonance\HttpResponderInterface;
-use Distantmagic\Resonance\PsrStringStream;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Throwable;
@@ -41,7 +40,7 @@ final readonly class ServerError extends Error
         return $response
             ->withStatus(500)
             ->withHeader('content-type', ContentType::TextPlain->value)
-            ->withBody(new PsrStringStream((string) $throwable))
+            ->withBody($this->createStream($throwable))
         ;
     }
 }
diff --git a/src/HttpResponder/Json.php b/src/HttpResponder/Json.php
index 3eb39ec9..1b259d50 100644
--- a/src/HttpResponder/Json.php
+++ b/src/HttpResponder/Json.php
@@ -6,7 +6,6 @@ namespace Distantmagic\Resonance\HttpResponder;
 
 use Distantmagic\Resonance\ContentType;
 use Distantmagic\Resonance\HttpResponder;
-use Distantmagic\Resonance\PsrStringStream;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 
@@ -19,7 +18,7 @@ readonly class Json extends HttpResponder
         return $response
             ->withStatus(200)
             ->withHeader('content-type', ContentType::ApplicationJson->value)
-            ->withBody(new PsrStringStream($this->json))
+            ->withBody($this->createStream($this->json))
         ;
     }
 }
diff --git a/src/HttpResponder/NotAcceptable.php b/src/HttpResponder/NotAcceptable.php
index ac97b874..d28a2a77 100644
--- a/src/HttpResponder/NotAcceptable.php
+++ b/src/HttpResponder/NotAcceptable.php
@@ -7,7 +7,6 @@ namespace Distantmagic\Resonance\HttpResponder;
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\ContentType;
 use Distantmagic\Resonance\HttpResponder;
-use Distantmagic\Resonance\PsrStringStream;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 
@@ -19,7 +18,7 @@ final readonly class NotAcceptable extends HttpResponder
         return $response
             ->withStatus(406)
             ->withHeader('content-type', ContentType::TextPlain->value)
-            ->withBody(new PsrStringStream('406'))
+            ->withBody($this->createStream('406'))
         ;
     }
 }
diff --git a/src/HttpResponderInterface.php b/src/HttpResponderInterface.php
index 4947b4a8..a9bd5700 100644
--- a/src/HttpResponderInterface.php
+++ b/src/HttpResponderInterface.php
@@ -6,9 +6,8 @@ namespace Distantmagic\Resonance;
 
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
-use Psr\Http\Server\RequestHandlerInterface;
 
-interface HttpResponderInterface extends RequestHandlerInterface
+interface HttpResponderInterface
 {
     public function respond(ServerRequestInterface $request, ResponseInterface $response): HttpInterceptableInterface|ResponseInterface|self;
 }
diff --git a/src/PsrStringStream.php b/src/PsrStringStream.php
index 003a8ac5..8c9dc8a3 100644
--- a/src/PsrStringStream.php
+++ b/src/PsrStringStream.php
@@ -5,10 +5,16 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance;
 
 use Psr\Http\Message\StreamInterface;
+use Stringable;
 
 readonly class PsrStringStream implements StreamInterface
 {
-    public function __construct(private string $contents) {}
+    private string $contents;
+
+    public function __construct(string|Stringable $contents)
+    {
+        $this->contents = (string) $contents;
+    }
 
     public function __toString(): string
     {
-- 
GitLab