Completed
Push — 8.2 ( 737f16...22bfc7 )
by David
17:32
created

SplashDefaultRouter   C

Complexity

Total Complexity 36

Size/Duplication

Total Lines 396
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 22

Importance

Changes 0
Metric Value
wmc 36
lcom 1
cbo 22
dl 0
loc 396
rs 5.0946
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 3
A setHttp400Handler() 0 6 1
A setHttp404Handler() 0 6 1
A setHttp500Handler() 0 6 1
C __invoke() 0 24 7
D route() 0 116 13
A purgeExpiredRoutes() 0 21 4
A getSplashActionsList() 0 12 2
A generateUrlNode() 0 9 2
A purgeUrlsCache() 0 4 1
A process() 0 9 1
1
<?php
2
3
namespace Mouf\Mvc\Splash\Routers;
4
5
use Cache\Adapter\Void\VoidCachePool;
6
use Interop\Container\ContainerInterface;
7
use Interop\Http\ServerMiddleware\DelegateInterface;
8
use Interop\Http\ServerMiddleware\MiddlewareInterface;
9
use Mouf\Mvc\Splash\Controllers\Http400HandlerInterface;
10
use Mouf\Mvc\Splash\Controllers\Http404HandlerInterface;
11
use Mouf\Mvc\Splash\Controllers\Http500HandlerInterface;
12
use Mouf\Mvc\Splash\Exception\BadRequestException;
13
use Mouf\Mvc\Splash\Exception\PageNotFoundException;
14
use Mouf\Mvc\Splash\Services\ParameterFetcher;
15
use Mouf\Mvc\Splash\Services\ParameterFetcherRegistry;
16
use Mouf\Mvc\Splash\Services\UrlProviderInterface;
17
use Mouf\Mvc\Splash\Utils\SplashException;
18
use Psr\Cache\CacheItemPoolInterface;
19
use Psr\Http\Message\ResponseInterface;
20
use Psr\Http\Message\ServerRequestInterface;
21
use Mouf\Mvc\Splash\Store\SplashUrlNode;
22
use Psr\Log\LoggerInterface;
23
use Mouf\Mvc\Splash\Services\SplashRequestContext;
24
use Mouf\Mvc\Splash\Services\SplashUtils;
25
use Psr\Log\NullLogger;
26
use Zend\Diactoros\Response;
27
use Zend\Diactoros\Response\RedirectResponse;
28
29
class SplashDefaultRouter implements MiddlewareInterface
30
{
31
    /**
32
     * The container that will be used to fetch controllers.
33
     *
34
     * @var ContainerInterface
35
     */
36
    private $container;
37
38
    /**
39
     * List of objects that provide routes.
40
     *
41
     * @var UrlProviderInterface[]
42
     */
43
    private $routeProviders = [];
44
45
    /**
46
     * The logger used by Splash.
47
     *
48
     * @var LoggerInterface
49
     */
50
    private $log;
51
52
    /**
53
     * Splash uses the cache service to store the URL mapping (the mapping between a URL and its controller/action).
54
     *
55
     * @var CacheItemPoolInterface
56
     */
57
    private $cachePool;
58
59
    /**
60
     * The default mode for Splash. Can be one of 'weak' (controllers are allowed to output HTML), or 'strict' (controllers
61
     * are requested to return a ResponseInterface object).
62
     *
63
     * @var string
64
     */
65
    private $mode;
66
67
    /**
68
     * In debug mode, Splash will display more accurate messages if output starts (in strict mode).
69
     *
70
     * @var bool
71
     */
72
    private $debug;
73
74
    /**
75
     * @var ParameterFetcher[]
76
     */
77
    private $parameterFetcherRegistry;
78
79
    /**
80
     * The base URL of the application (from which the router will start routing).
81
     *
82
     * @var string
83
     */
84
    private $rootUrl;
85
86
    /**
87
     * (optional) Handles HTTP 400 status code.
88
     *
89
     * @var Http400HandlerInterface
90
     */
91
    private $http400Handler;
92
93
    /**
94
     * (optional) Handles HTTP 404 status code (if no $out provided).
95
     *
96
     * @var Http404HandlerInterface
97
     */
98
    private $http404Handler;
99
100
    /**
101
     * (optional) Handles HTTP 500 status code.
102
     *
103
     * @var Http500HandlerInterface
104
     */
105
    private $http500Handler;
106
107
    /**
108
     * @Important
109
     *
110
     * @param ContainerInterface       $container                The container that will be used to fetch controllers.
111
     * @param UrlProviderInterface[]   $routeProviders
112
     * @param ParameterFetcherRegistry $parameterFetcherRegistry
113
     * @param CacheItemPoolInterface   $cachePool                Splash uses the cache service to store the URL mapping (the mapping between a URL and its controller/action)
114
     * @param LoggerInterface          $log                      The logger used by Splash
115
     * @param string                   $mode                     The default mode for Splash. Can be one of 'weak' (controllers are allowed to output HTML), or 'strict' (controllers are requested to return a ResponseInterface object).
116
     * @param bool                     $debug                    In debug mode, Splash will display more accurate messages if output starts (in strict mode)
117
     * @param string                   $rootUrl
118
     */
119
    public function __construct(ContainerInterface $container, array $routeProviders, ParameterFetcherRegistry $parameterFetcherRegistry, CacheItemPoolInterface $cachePool = null, LoggerInterface $log = null, string $mode = SplashUtils::MODE_STRICT, bool $debug = true, string $rootUrl = '/')
120
    {
121
        $this->container = $container;
122
        $this->routeProviders = $routeProviders;
123
        $this->parameterFetcherRegistry = $parameterFetcherRegistry;
0 ignored issues
show
Documentation Bug introduced by
It seems like $parameterFetcherRegistry of type object<Mouf\Mvc\Splash\S...rameterFetcherRegistry> is incompatible with the declared type array<integer,object<Mou...ices\ParameterFetcher>> of property $parameterFetcherRegistry.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
124
        $this->cachePool = $cachePool === null ? new VoidCachePool() : $cachePool;
125
        $this->log = $log === null ? new NullLogger() : $log;
126
        $this->mode = $mode;
127
        $this->debug = $debug;
128
        $this->rootUrl = rtrim($rootUrl, '/').'/';
129
    }
130
131
    /**
132
     * @param Http400HandlerInterface $http400Handler
133
     */
134
    public function setHttp400Handler($http400Handler)
135
    {
136
        $this->http400Handler = $http400Handler;
137
138
        return $this;
139
    }
140
141
    /**
142
     * @param Http404HandlerInterface $http404Handler
143
     */
144
    public function setHttp404Handler($http404Handler)
145
    {
146
        $this->http404Handler = $http404Handler;
147
148
        return $this;
149
    }
150
151
    /**
152
     * @param Http500HandlerInterface $http500Handler
153
     */
154
    public function setHttp500Handler($http500Handler)
155
    {
156
        $this->http500Handler = $http500Handler;
157
158
        return $this;
159
    }
160
161
    /**
162
     * Process an incoming request and/or response.
163
     *
164
     * Accepts a server-side request and a response instance, and does
165
     * something with them.
166
     *
167
     * If the response is not complete and/or further processing would not
168
     * interfere with the work done in the middleware, or if the middleware
169
     * wants to delegate to another process, it can use the `$out` callable
170
     * if present.
171
     *
172
     * If the middleware does not return a value, execution of the current
173
     * request is considered complete, and the response instance provided will
174
     * be considered the response to return.
175
     *
176
     * Alternately, the middleware may return a response instance.
177
     *
178
     * Often, middleware will `return $out();`, with the assumption that a
179
     * later middleware will return a response.
180
     *
181
     * @param ServerRequestInterface $request
182
     * @param ResponseInterface      $response
183
     * @param null|callable          $out
184
     *
185
     * @return null|ResponseInterface
186
     */
187
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $out = null)
188
    {
189
        try {
190
            return $this->route($request, $response, $out);
191
        } catch (BadRequestException $e) {
192
            if ($this->http400Handler !== null) {
193
                return $this->http400Handler->badRequest($e, $request);
194
            } else {
195
                throw $e;
196
            }
197
        } catch (PageNotFoundException $e) {
198
            if ($this->http404Handler !== null) {
199
                return $this->http404Handler->pageNotFound($request);
200
            } else {
201
                throw $e;
202
            }
203
        } catch (\Throwable $t) {
204
            if ($this->http500Handler !== null) {
205
                return $this->http500Handler->serverError($t, $request);
206
            } else {
207
                throw $t;
208
            }
209
        }
210
    }
211
212
    /**
213
     * @param ServerRequestInterface $request
214
     * @param ResponseInterface      $response
215
     * @param callable|null          $out
216
     *
217
     * @return ResponseInterface
218
     */
219
    private function route(ServerRequestInterface $request, ResponseInterface $response, callable $out = null, $retry = false) : ResponseInterface
220
    {
221
        $this->purgeExpiredRoutes();
222
223
        $urlNodesCacheItem = $this->cachePool->getItem('splashUrlNodes');
224
        if (!$urlNodesCacheItem->isHit()) {
225
            // No value in cache, let's get the URL nodes
226
            $urlsList = $this->getSplashActionsList();
227
            $urlNodes = $this->generateUrlNode($urlsList);
228
            $urlNodesCacheItem->set($urlNodes);
229
            $this->cachePool->save($urlNodesCacheItem);
230
        }
231
232
        $urlNodes = $urlNodesCacheItem->get();
233
        /* @var $urlNodes SplashUrlNode */
234
235
        $request_path = $request->getUri()->getPath();
236
237
        $pos = strpos($request_path, $this->rootUrl);
238
        if ($pos === false) {
239
            throw new SplashException('Error: the prefix of the web application "'.$this->rootUrl.'" was not found in the URL. The application must be misconfigured. Check the ROOT_URL parameter in your config.php file at the root of your project. It should have the same value as the RewriteBase parameter in your .htaccess file. Requested URL : "'.$request_path.'"');
240
        }
241
242
        $tailing_url = substr($request_path, $pos + strlen($this->rootUrl));
243
        $tailing_url = urldecode($tailing_url);
244
        $splashRoute = $urlNodes->walk($tailing_url, $request);
245
246
        if ($splashRoute === null) {
247
            // No route found. Let's try variants with or without trailing / if we are in a GET.
248
            if ($request->getMethod() === 'GET') {
249
                // If there is a trailing /, let's remove it and retry
250
                if (strrpos($tailing_url, '/') === strlen($tailing_url) - 1) {
251
                    $url = substr($tailing_url, 0, -1);
252
                    $splashRoute = $urlNodes->walk($url, $request);
253
                } else {
254
                    $url = $tailing_url.'/';
255
                    $splashRoute = $urlNodes->walk($url, $request);
256
                }
257
258
                if ($splashRoute !== null) {
259
                    // If a route does match, let's make a redirect.
260
                    return new RedirectResponse($this->rootUrl.$url);
261
                }
262
            }
263
264
            if ($this->debug === false || $retry === true) {
265
                // No route found, let's pass control to the next middleware.
266
                if ($out !== null) {
267
                    return $out($request, $response);
268
                } else {
269
                    $this->log->debug('Found no route for URL {url}.', [
270
                        'url' => $request_path,
271
                    ]);
272
                    throw PageNotFoundException::create($tailing_url);
273
                }
274
            } else {
275
                // We have a 404, but we are in debug mode and have not retried yet...
276
                // Let's purge the cache and retry!
277
                $this->purgeUrlsCache();
278
279
                return $this->route($request, $response, $out, true);
280
            }
281
        }
282
283
        // Is the route still valid according to the cache?
284
        if (!$splashRoute->isCacheValid()) {
285
            // The route is invalid! Let's purge the cache and retry!
286
            $this->purgeUrlsCache();
287
288
            return $this($request, $response, $out);
289
        }
290
291
        $controller = $this->container->get($splashRoute->getControllerInstanceName());
292
        $action = $splashRoute->getMethodName();
293
294
        $this->log->debug('Routing URL {url} to controller instance {controller} and action {action}', [
295
            'url' => $request_path,
296
            'controller' => $splashRoute->getControllerInstanceName(),
297
            'action' => $action,
298
        ]);
299
300
        $filters = $splashRoute->getFilters();
301
302
        $middlewareCaller = function (ServerRequestInterface $request, ResponseInterface $response) use ($controller, $action, $splashRoute) {
0 ignored issues
show
Unused Code introduced by
The parameter $response is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
303
            // Let's recreate a new context object (because request can be modified by the filters)
304
            $context = new SplashRequestContext($request);
305
            $context->setUrlParameters($splashRoute->getFilledParameters());
306
            // Let's pass everything to the controller:
307
            $args = $this->parameterFetcherRegistry->toArguments($context, $splashRoute->getParameters());
0 ignored issues
show
Bug introduced by
The method toArguments cannot be called on $this->parameterFetcherRegistry (of type array<integer,object<Mou...ices\ParameterFetcher>>).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
308
309
            try {
310
                $response = SplashUtils::buildControllerResponse(
311
                    function () use ($controller, $action, $args) {
312
                        return $controller->$action(...$args);
313
                    },
314
                    $this->mode,
315
                    $this->debug
316
                );
317
            } catch (SplashException $e) {
318
                throw new SplashException($e->getMessage(). ' (in '.$splashRoute->getControllerInstanceName().'->'.$splashRoute->getMethodName().')', $e->getCode(), $e);
319
            }
320
            return $response;
321
        };
322
323
        // Apply filters
324
        for ($i = count($filters) - 1; $i >= 0; --$i) {
325
            $filter = $filters[$i];
326
            $middlewareCaller = function (ServerRequestInterface $request, ResponseInterface $response) use ($middlewareCaller, $filter) {
327
                return $filter($request, $response, $middlewareCaller, $this->container);
328
            };
329
        }
330
331
        $response = $middlewareCaller($request, $response);
332
333
        return $response;
334
    }
335
336
    /**
337
     * Purges the cache if one of the url providers tells us to.
338
     */
339
    private function purgeExpiredRoutes()
340
    {
341
        $expireTag = '';
342
        foreach ($this->routeProviders as $routeProvider) {
343
            /* @var $routeProvider UrlProviderInterface */
344
            $expireTag .= $routeProvider->getExpirationTag();
345
        }
346
347
        $value = md5($expireTag);
348
349
        $urlNodesCacheItem = $this->cachePool->getItem('splashExpireTag');
350
351
        if ($urlNodesCacheItem->isHit() && $urlNodesCacheItem->get() === $value) {
352
            return;
353
        }
354
355
        $this->purgeUrlsCache();
356
357
        $urlNodesCacheItem->set($value);
358
        $this->cachePool->save($urlNodesCacheItem);
359
    }
360
361
    /**
362
     * Returns the list of all SplashActions.
363
     * This call is LONG and should be cached.
364
     *
365
     * @return array<SplashAction>
366
     */
367
    public function getSplashActionsList()
368
    {
369
        $urls = array();
370
371
        foreach ($this->routeProviders as $routeProvider) {
372
            /* @var $routeProvider UrlProviderInterface */
373
            $tmpUrlList = $routeProvider->getUrlsList(null);
374
            $urls = array_merge($urls, $tmpUrlList);
375
        }
376
377
        return $urls;
378
    }
379
380
    /**
381
     * Generates the URLNodes from the list of URLS.
382
     * URLNodes are a very efficient way to know whether we can access our page or not.
383
     *
384
     * @param array<SplashAction> $urlsList
385
     *
386
     * @return SplashUrlNode
387
     */
388
    private function generateUrlNode($urlsList)
389
    {
390
        $urlNode = new SplashUrlNode();
391
        foreach ($urlsList as $splashAction) {
392
            $urlNode->registerCallback($splashAction);
393
        }
394
395
        return $urlNode;
396
    }
397
398
    /**
399
     * Purges the urls cache.
400
     */
401
    public function purgeUrlsCache()
402
    {
403
        $this->cachePool->deleteItem('splashUrlNodes');
404
    }
405
406
    /**
407
     * Process an incoming server request and return a response, optionally delegating
408
     * to the next middleware component to create the response.
409
     *
410
     * @param ServerRequestInterface $request
411
     * @param DelegateInterface $delegate
412
     *
413
     * @return ResponseInterface
414
     */
415
    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
416
    {
417
        // create a dummy response to keep compatibility with old middlewares.
418
        $response = new Response();
419
420
        return $this($request, $response, function($request) use ($delegate) {
421
            return $delegate->process($request);
422
        });
423
    }
424
}
425