From b30f410ea02b2330eef871a2eb59ad022fdfa102 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Fri, 23 Jun 2017 11:17:30 +1200 Subject: [PATCH] API: Deprecate RequestFilter. NEW: Allow application of HTTPMiddleware to Director. Director can now use the same HTTPMiddleware objects as the app object. They can be applied either globally or pre-rule. --- _config/routes.yml | 2 + src/Control/Director.php | 99 ++++++++++++++++++++++++-------- src/Control/RequestProcessor.php | 31 ++++++---- 3 files changed, 98 insertions(+), 34 deletions(-) diff --git a/_config/routes.yml b/_config/routes.yml index 86c35b233..f15949722 100644 --- a/_config/routes.yml +++ b/_config/routes.yml @@ -2,6 +2,8 @@ Name: rootroutes --- SilverStripe\Control\Director: + middlewares: + RequestProcessor: 'SilverStripe\Control\RequestProcessor' rules: '': SilverStripe\Control\Controller --- diff --git a/src/Control/Director.php b/src/Control/Director.php index f5f2d920f..59fb66efd 100644 --- a/src/Control/Director.php +++ b/src/Control/Director.php @@ -131,24 +131,12 @@ class Director implements TemplateGlobalProvider } } - // Pre-request - $output = RequestProcessor::singleton()->preRequest($request); - if ($output === false) { - return new HTTPResponse(_t(__CLASS__.'.INVALID_REQUEST', 'Invalid request'), 400); - } - // Generate output $result = static::handleRequest($request); // Save session data. Note that save() will start/resume the session if required. $request->getSession()->save(); - // Post-request handling - $postRequest = RequestProcessor::singleton()->postRequest($request, $result); - if ($postRequest === false) { - return new HTTPResponse(_t(__CLASS__ . '.REQUEST_ABORTED', 'Request aborted'), 500); - } - // Return return $result; } @@ -351,6 +339,14 @@ class Director implements TemplateGlobalProvider { $rules = Director::config()->uninherited('rules'); + // Get global middlewares + $middlewares = Director::config()->uninherited('middlewares') ?: []; + + // Default handler - mo URL rules matched, so return a 404 error. + $handler = function () { + return new HTTPResponse('No URL rule was matched', 404); + }; + foreach ($rules as $pattern => $controllerOptions) { // Normalise route rule if (is_string($controllerOptions)) { @@ -361,6 +357,18 @@ class Director implements TemplateGlobalProvider } } + // Add controller-specific middlewares + if (isset($controllerOptions['Middlewares'])) { + // Force to array + if (!is_array($controllerOptions['Middlewares'])) { + $controllerOptions['Middlewares'] = [$controllerOptions['Middlewares']]; + } + $middlewares = array_merge($middlewares, $controllerOptions['Middlewares']); + } + + // Remove null middlewares (may be included due to limitatons of config yml) + $middlewares = array_filter($middlewares); + // Match pattern $arguments = $request->match($pattern, true); if ($arguments !== false) { @@ -373,28 +381,71 @@ class Director implements TemplateGlobalProvider $request->shift($controllerOptions['_PopTokeniser']); } - // Handle redirection + // Handler for redirection if (isset($arguments['Redirect'])) { - // Redirection - $response = new HTTPResponse(); - $response->redirect(static::absoluteURL($arguments['Redirect'])); - return $response; + $handler = function () use ($arguments) { + // Redirection + $response = new HTTPResponse(); + $response->redirect(static::absoluteURL($arguments['Redirect'])); + return $response; + }; + break; } // Find the controller name $controller = $arguments['Controller']; $controllerObj = Injector::inst()->create($controller); - try { - return $controllerObj->handleRequest($request); - } catch (HTTPResponse_Exception $responseException) { - return $responseException->getResponse(); - } + // Handler for calling a controller + $handler = function ($request) use ($controllerObj) { + try { + return $controllerObj->handleRequest($request); + } catch (HTTPResponse_Exception $responseException) { + return $responseException->getResponse(); + } + }; + break; } } - // No URL rules matched, so return a 404 error. - return new HTTPResponse('No URL rule was matched', 404); + // Call the handler with the given middlewares + return self::callWithMiddlewares( + $request, + $middlewares, + $handler + ); + } + + /** + * Call the given request handler with the given middlewares + * Middlewares are specified as Injector service names + * + * @param $request The request to pass to the handler + * @param $middlewareNames The services names of the middlewares to apply + * @param $handler The request handler + */ + protected static function callWithMiddlewares(HTTPRequest $request, array $middlewareNames, callable $handler) + { + $next = $handler; + + if ($middlewareNames) { + $middlewares = array_map( + function ($name) { + return Injector::inst()->get($name); + }, + $middlewareNames + ); + + // Reverse middlewares + /** @var HTTPMiddleware $middleware */ + foreach (array_reverse($middlewares) as $middleware) { + $next = function ($request) use ($middleware, $next) { + return $middleware->process($request, $next); + }; + } + } + + return $next($request); } /** diff --git a/src/Control/RequestProcessor.php b/src/Control/RequestProcessor.php index a1e0cad1a..fa6d319ae 100644 --- a/src/Control/RequestProcessor.php +++ b/src/Control/RequestProcessor.php @@ -3,11 +3,13 @@ namespace SilverStripe\Control; use SilverStripe\Core\Injector\Injectable; +use SilverStripe\Dev\Deprecation; /** - * Represents a request processer that delegates pre and post request handling to nested request filters + * Middleware that provides back-support for the deprecated RequestFilter API. + * You should use HTTPMiddleware directly instead. */ -class RequestProcessor implements RequestFilter +class RequestProcessor implements HTTPMiddleware { use Injectable; @@ -33,25 +35,34 @@ class RequestProcessor implements RequestFilter $this->filters = $filters; } - public function preRequest(HTTPRequest $request) + /** + * @inheritdoc + */ + public function process(HTTPRequest $request, callable $delegate) { + if ($this->filters) { + Deprecation::notice( + '4.0', + 'Deprecated RequestFilters are in use. Apply HTTPMiddleware to Director.middlewares instead.' + ); + } + foreach ($this->filters as $filter) { $res = $filter->preRequest($request); if ($res === false) { - return false; + return new HTTPResponse(_t(__CLASS__.'.INVALID_REQUEST', 'Invalid request'), 400); } } - return null; - } - public function postRequest(HTTPRequest $request, HTTPResponse $response) - { + $response = $delegate($request); + foreach ($this->filters as $filter) { $res = $filter->postRequest($request, $response); if ($res === false) { - return false; + return new HTTPResponse(_t(__CLASS__ . '.REQUEST_ABORTED', 'Request aborted'), 500); } } - return null; + + return $response; } }