neos / flow-development-collection

The unified repository containing the Flow core packages, used for Flow development.
https://flow.neos.io/
MIT License
139 stars 188 forks source link

Convert existing HTTP Components to PSR-15 middleware #2019

Closed albe closed 3 years ago

albe commented 4 years ago

This is a follow-up to #1889 that is meant to track the status of "phasing out" the existing HTTP Components.

        'preprocess':
          position: 'before process'
          chain:
            'trustedProxies':
              position: 'start'
              component: 'Neos\Flow\Http\Component\TrustedProxiesComponent'
            'getSessionCookieFromRequest':
              position: 'after trustedProxies'
              component: 'Neos\Flow\Session\Http\SessionRequestComponent'

        'process':
          chain:
            'routing':
              position: 'start'
              component: 'Neos\Flow\Mvc\Routing\RoutingComponent'
            'parseBody':
              position: 'before dispatching'
              component: 'Neos\Flow\Http\Component\RequestBodyParsingComponent'
            'prepareActionRequest':
              position: 'after parseBody'
              component: 'Neos\Flow\Mvc\PrepareMvcRequestComponent'
            'dispatching':
              component: 'Neos\Flow\Mvc\DispatchComponent'
            'replaceHttpResponse':
              position: 'after dispatching'
              component: 'Neos\Flow\Http\Component\ReplaceHttpResponseComponent'
            'setHeader':
              position: 'after dispatching'
              component: 'Neos\Flow\Http\Component\SetHeaderComponent'
            'securityEntryPoint':
              position: 'after setHeaders'
              component: 'Neos\Flow\Http\Component\SecurityEntryPointComponent'

        'postprocess':
          chain:
            'setSessionCookie':
              position: 'start'
              component: 'Neos\Flow\Session\Http\SessionResponseComponent'
            'flashMessages':
              position: 'after setSessionCookie'
              component: 'Neos\Flow\Mvc\FlashMessage\FlashMessageComponent'
            'poweredByHeader':
              component: 'Neos\Flow\Http\Component\PoweredByComponent'
            'standardsCompliance':
              position: 'end'
              component: 'Neos\Flow\Http\Component\StandardsComplianceComponent'

This is the current (as of Flow 6.2) list of existing configured HTTP components. The order to convert them should be starting from pre-process, since the PSR-15 middlewares invoke the component chain. Hence the first step is the TrustedProxiesComponent. Also, some Components can probably be merged together (SessionRequest+Response), some need to be moved into the MVC stack maybe. We'll need to play and adapt there.

sorenmalling commented 4 years ago

I've been giving RoutingComponent a try-out. It's fairly tight copuled to PrepareMvcRequestComponent and DispatchComponent and even tied to SecurityEntryPointComponent, from inside the DispatchComponent.

Is this "MVC Dispatching Middleware" something we should focus on here and now while we have the engine open?

sorenmalling commented 4 years ago

@albe Is the approach correct for #2153 and #2154 with a single commit per middleware and Settings.yaml change?

albe commented 4 years ago

I've been giving RoutingComponent a try-out. It's fairly tight copuled to PrepareMvcRequestComponent and DispatchComponent and even tied to SecurityEntryPointComponent, from inside the DispatchComponent.

Let's take a look at that together maybe.

Is this "MVC Dispatching Middleware" something we should focus on here and now while we have the engine open?

If we have finished PSR-15 and everything around that is safe & sound. But low prio and from a gut feeling it will probably not happen for 7.0

Is the approach correct for #2153 and #2154 with a single commit per middleware and Settings.yaml change?

Sure, why not. We could have attempted to do all in one PR, but this can easily grow out of hand, so this is maybe the most efficient way for review. Just need to make sure all those PRs integrate well together, so we should merge and then rebase the others every once in a while.

sorenmalling commented 4 years ago

Let's take a look at that together maybe.

Tuesday and wednesday er good for me - how about you?

Just need to make sure all those PRs integrate well together, so we should merge and then rebase the others every once in a while

Great idea - let's rebase on top of other middleware commits before merging the next 👍

bwaidelich commented 4 years ago

I just had a great session with @albe and @sorenmalling about the fact that ActionResponse::setComponentParameter() won't work with the middleware approach since it is only possible to pass information (other than the HTTP response) down into the middleware chain, but the Dispatch MW has to be the inner most.

A few options we discussed:

Re-introduce some kind of global context

...similar to the ComponentContext... We don't like that solution because it defeats the idea of having a clean PSR-15 implementation

Introduce a separate MVC Middleware Sub-Chain

This would introduce a new level of complexity – and probably only for really edgy edge cases that can be solved otherwise (see below)

Replace ActionResponse::setComponentParameter()

Our favorite solution for now, even if more breaking (see comment regarding b/c)

new DispatchMiddleware

This is how the DispatchMiddleware could look like:

<?php
namespace Neos\Flow\Mvc;

/*
 * This file is part of the Neos.Flow package.
 *
 * (c) Contributors of the Neos Project - www.neos.io
 *
 * This package is Open Source Software. For the full copyright and license
 * information, please view the LICENSE file which was distributed with this
 * source code.
 */

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Http\ServerRequestAttributes;
use Neos\Flow\Security\Context;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
 * A dispatch component
 */
class DispatchMiddleware implements MiddlewareInterface
{
    /**
     * @Flow\Inject
     * @var Dispatcher
     */
    protected $dispatcher;

    /**
     * @Flow\Inject
     * @var ActionRequestFactory
     */
    protected $actionRequestFactory;

    /**
     * @Flow\Inject
     * @var Context
     */
    protected $securityContext;

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $routingMatchResults = $request->getAttribute(ServerRequestAttributes::ROUTING_RESULTS) ?? [];
        $actionRequest = $this->actionRequestFactory->createActionRequest($request, $routingMatchResults);

        // can we move this somewhere else?
        $this->securityContext->setRequest($actionRequest);

        $actionResponse = new ActionResponse();
        $this->dispatcher->dispatch($actionRequest, $actionResponse);
        return $actionResponse->buildHttpResponse();
    }
}

Adjusted ActionResponse

diff --git a/Neos.Flow/Classes/Mvc/ActionResponse.php b/Neos.Flow/Classes/Mvc/ActionResponse.php
index 8cbbced88..538b3ccef 100644
--- a/Neos.Flow/Classes/Mvc/ActionResponse.php
+++ b/Neos.Flow/Classes/Mvc/ActionResponse.php
@@ -1,14 +1,16 @@
 <?php
 namespace Neos\Flow\Mvc;

+use GuzzleHttp\Psr7\Response;
+use GuzzleHttp\Psr7\Stream;
+use Neos\Flow\Annotations as Flow;
+use Neos\Flow\Http\Component\ReplaceHttpResponseComponent;
+use Neos\Flow\Http\Component\SetHeaderComponent;
 use Neos\Flow\Http\Cookie;
 use Psr\Http\Message\ResponseInterface;
-use function GuzzleHttp\Psr7\stream_for;
-use Neos\Flow\Annotations as Flow;
-use Neos\Flow\Http\Component\ComponentContext;
 use Psr\Http\Message\StreamInterface;
 use Psr\Http\Message\UriInterface;
-use GuzzleHttp\Psr7\Stream;
+use function GuzzleHttp\Psr7\stream_for;

 /**
  * The minimal MVC response object.
@@ -26,9 +28,14 @@ final class ActionResponse
     protected $content;

     /**
-     * @var array
+     * @var ResponseInterface|null
      */
-    protected $componentParameters = [];
+    protected $replacedHttpResponse;
+
+    /**
+     * @var string[][]
+     */
+    protected $httpHeaders = [];

     /**
      * @var UriInterface
@@ -147,32 +154,51 @@ final class ActionResponse
      * @param string $parameterName
      * @param mixed $value
      * @return void
-     * @api
+     * @deprecated since Flow 7.0 ... (todo explain)
      */
     public function setComponentParameter(string $componentClassName, string $parameterName, $value): void
     {
-        if (!isset($this->componentParameters[$componentClassName])) {
-            $this->componentParameters[$componentClassName] = [];
+        if ($componentClassName === SetHeaderComponent::class) {
+            $this->setHttpHeader($parameterName, $value);
+            return;
         }
-        $this->componentParameters[$componentClassName][$parameterName] = $value;
+        if ($componentClassName === ReplaceHttpResponseComponent::class && $parameterName === ReplaceHttpResponseComponent::PARAMETER_RESPONSE) {
+            $this->replaceHttpResponse($value);
+            return;
+        }
+        throw new \InvalidArgumentException('todo');
     }

     /**
-     * @return string
+     * TODO document
+     *
+     * @param string $headerName
+     * @param string|string[] $headerValue
      */
-    public function getContent(): string
+    public function setHttpHeader(string $headerName, $headerValue): void
     {
-        $content = $this->content->getContents();
-        $this->content->rewind();
-        return $content;
+        // todo validate $headerValue (string|string[])
+        $this->httpHeaders[$headerName] = $headerValue;
     }

     /**
-     * @return array
+     * TODO document
+     *
+     * @param ResponseInterface $response
      */
-    public function getComponentParameters(): array
+    public function replaceHttpResponse(ResponseInterface $response): void
     {
-        return $this->componentParameters;
+        $this->replacedHttpResponse = $response;
+    }
+
+    /**
+     * @return string
+     */
+    public function getContent(): string
+    {
+        $content = $this->content->getContents();
+        $this->content->rewind();
+        return $content;
     }

     /**
@@ -220,11 +246,11 @@ final class ActionResponse
         if ($this->redirectUri !== null) {
             $actionResponse->setRedirectUri($this->redirectUri);
         }
-
-        foreach ($this->componentParameters as $componentClass => $parameters) {
-            foreach ($parameters as $parameterName => $parameterValue) {
-                $actionResponse->setComponentParameter($componentClass, $parameterName, $parameterValue);
-            }
+        if ($this->replacedHttpResponse !== null) {
+            $actionResponse->replaceHttpResponse($this->replacedHttpResponse);
+        }
+        foreach ($this->httpHeaders as $headerName => $headerValue) {
+            $actionResponse->setHttpHeader($headerName, $headerValue);
         }
         foreach ($this->cookies as $cookie) {
             $actionResponse->setCookie($cookie);
@@ -234,57 +260,14 @@ final class ActionResponse
     }

     /**
-     * @param ComponentContext $componentContext
-     * @return ComponentContext
-     */
-    public function mergeIntoComponentContext(ComponentContext $componentContext): ComponentContext
-    {
-        $httpResponse = $componentContext->getHttpResponse();
-        if ($this->statusCode !== null) {
-            $httpResponse = $httpResponse->withStatus($this->statusCode);
-        }
-
-        if ($this->hasContent()) {
-            $httpResponse = $httpResponse->withBody($this->content);
-        }
-
-        if ($this->contentType) {
-            $httpResponse = $httpResponse->withHeader('Content-Type', $this->contentType);
-        }
-
-        if ($this->redirectUri) {
-            $httpResponse = $httpResponse->withHeader('Location', (string)$this->redirectUri);
-        }
-
-        foreach ($this->componentParameters as $componentClassName => $componentParameterGroup) {
-            foreach ($componentParameterGroup as $parameterName => $parameterValue) {
-                $componentContext->setParameter($componentClassName, $parameterName, $parameterValue);
-            }
-        }
-
-        foreach ($this->cookies as $cookie) {
-            $httpResponse = $httpResponse->withAddedHeader('Set-Cookie', (string)$cookie);
-        }
-
-        $componentContext->replaceHttpResponse($httpResponse);
-
-        return $componentContext;
-    }
-
-    /**
-     * Note this is a special use case method that will apply the internal properties (Content-Type, StatusCode, Location, Set-Cookie and Content)
-     * to the given PSR-7 Response and return a modified response. This is used to merge the ActionResponse properties into a possible HttpResponse
-     * created in a View (see ActionController::renderView()) because those would be overwritten otherwise. Note that any component parameters will
-     * still run through the component chain and will not be propagated here.
-     *
-     * WARNING: Should this ActionResponse contain body content it would replace any content in the given HttpReponse.
+     * TODO document
      *
-     * @param ResponseInterface $httpResponse
      * @return ResponseInterface
      * @internal
      */
-    public function applyToHttpResponse(ResponseInterface $httpResponse): ResponseInterface
+    public function buildHttpResponse(): ResponseInterface
     {
+        $httpResponse = $this->replacedHttpResponse ?? new Response();
         if ($this->statusCode !== null) {
             $httpResponse = $httpResponse->withStatus($this->statusCode);
         }
@@ -305,9 +288,14 @@ final class ActionResponse
             $httpResponse = $httpResponse->withAddedHeader('Set-Cookie', (string)$cookie);
         }

+        foreach ($this->httpHeaders as $headerName => $headerValue) {
+            $httpResponse = $httpResponse->withHeader($headerName, $headerValue);
+        }
+
         return $httpResponse;
     }

+
     /**
      * Does this action response have content?
      *