Completed
Push — fetcher_factories ( 10a56f...fd93ea )
by David
13:44
created

SplashDefaultRouter::getSplashActionsList()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 12
rs 9.4285
cc 2
eloc 6
nc 2
nop 0
1
<?php
2
3
namespace Mouf\Mvc\Splash\Routers;
4
5
use Cache\Adapter\Void\VoidCachePool;
6
use Interop\Container\ContainerInterface;
7
use Mouf\Mvc\Splash\Controllers\Http400HandlerInterface;
8
use Mouf\Mvc\Splash\Controllers\Http404HandlerInterface;
9
use Mouf\Mvc\Splash\Controllers\Http500HandlerInterface;
10
use Mouf\Mvc\Splash\Exception\BadRequestException;
11
use Mouf\Mvc\Splash\Exception\PageNotFoundException;
12
use Mouf\Mvc\Splash\Services\ParameterFetcher;
13
use Mouf\Mvc\Splash\Services\ParameterFetcherRegistry;
14
use Mouf\Mvc\Splash\Services\UrlProviderInterface;
15
use Mouf\Mvc\Splash\Utils\SplashException;
16
use Psr\Cache\CacheItemPoolInterface;
17
use Psr\Http\Message\ResponseInterface;
18
use Psr\Http\Message\ServerRequestInterface;
19
use Mouf\Mvc\Splash\Store\SplashUrlNode;
20
use Psr\Log\LoggerInterface;
21
use Mouf\Mvc\Splash\Services\SplashRequestContext;
22
use Mouf\Mvc\Splash\Services\SplashUtils;
23
use Psr\Log\NullLogger;
24
use Zend\Diactoros\Response\RedirectResponse;
25
use Zend\Stratigility\MiddlewareInterface;
26
27
class SplashDefaultRouter implements MiddlewareInterface
28
{
29
    /**
30
     * The container that will be used to fetch controllers.
31
     *
32
     * @var ContainerInterface
33
     */
34
    private $container;
35
36
    /**
37
     * List of objects that provide routes.
38
     *
39
     * @var UrlProviderInterface[]
40
     */
41
    private $routeProviders = [];
42
43
    /**
44
     * The logger used by Splash.
45
     *
46
     * @var LoggerInterface
47
     */
48
    private $log;
49
50
    /**
51
     * Splash uses the cache service to store the URL mapping (the mapping between a URL and its controller/action).
52
     *
53
     * @var CacheItemPoolInterface
54
     */
55
    private $cachePool;
56
57
    /**
58
     * The default mode for Splash. Can be one of 'weak' (controllers are allowed to output HTML), or 'strict' (controllers
59
     * are requested to return a ResponseInterface object).
60
     *
61
     * @var string
62
     */
63
    private $mode;
64
65
    /**
66
     * In debug mode, Splash will display more accurate messages if output starts (in strict mode).
67
     *
68
     * @var bool
69
     */
70
    private $debug;
71
72
    /**
73
     * @var ParameterFetcher[]
74
     */
75
    private $parameterFetcherRegistry;
76
77
    /**
78
     * The base URL of the application (from which the router will start routing).
79
     *
80
     * @var string
81
     */
82
    private $rootUrl;
83
84
    /**
85
     * (optional) Handles HTTP 400 status code.
86
     *
87
     * @var Http400HandlerInterface
88
     */
89
    private $http400Handler;
90
91
    /**
92
     * (optional) Handles HTTP 404 status code (if no $out provided).
93
     *
94
     * @var Http404HandlerInterface
95
     */
96
    private $http404Handler;
97
98
    /**
99
     * (optional) Handles HTTP 500 status code.
100
     *
101
     * @var Http500HandlerInterface
102
     */
103
    private $http500Handler;
104
105
    /**
106
     * @Important
107
     *
108
     * @param ContainerInterface       $container                The container that will be used to fetch controllers.
109
     * @param UrlProviderInterface[]   $routeProviders
110
     * @param ParameterFetcherRegistry $parameterFetcherRegistry
111
     * @param CacheItemPoolInterface   $cachePool                Splash uses the cache service to store the URL mapping (the mapping between a URL and its controller/action)
112
     * @param LoggerInterface          $log                      The logger used by Splash
113
     * @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).
114
     * @param bool                     $debug                    In debug mode, Splash will display more accurate messages if output starts (in strict mode)
115
     * @param string                   $rootUrl
116
     */
117
    public function __construct(ContainerInterface $container, array $routeProviders, ParameterFetcherRegistry $parameterFetcherRegistry, CacheItemPoolInterface $cachePool = null, LoggerInterface $log = null, $mode = SplashUtils::MODE_STRICT, $debug = true, $rootUrl = '/')
118
    {
119
        $this->container = $container;
120
        $this->routeProviders = $routeProviders;
121
        $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...
122
        $this->cachePool = $cachePool === null ? new VoidCachePool() : $cachePool;
123
        $this->log = $log === null ? new NullLogger() : $log;
124
        $this->mode = $mode;
125
        $this->debug = $debug;
126
        $this->rootUrl = $rootUrl;
127
    }
128
129
    /**
130
     * @param Http400HandlerInterface $http400Handler
131
     */
132
    public function setHttp400Handler($http400Handler)
133
    {
134
        $this->http400Handler = $http400Handler;
135
136
        return $this;
137
    }
138
139
    /**
140
     * @param Http404HandlerInterface $http404Handler
141
     */
142
    public function setHttp404Handler($http404Handler)
143
    {
144
        $this->http404Handler = $http404Handler;
145
146
        return $this;
147
    }
148
149
    /**
150
     * @param Http500HandlerInterface $http500Handler
151
     */
152
    public function setHttp500Handler($http500Handler)
153
    {
154
        $this->http500Handler = $http500Handler;
155
156
        return $this;
157
    }
158
159
    /**
160
     * Process an incoming request and/or response.
161
     *
162
     * Accepts a server-side request and a response instance, and does
163
     * something with them.
164
     *
165
     * If the response is not complete and/or further processing would not
166
     * interfere with the work done in the middleware, or if the middleware
167
     * wants to delegate to another process, it can use the `$out` callable
168
     * if present.
169
     *
170
     * If the middleware does not return a value, execution of the current
171
     * request is considered complete, and the response instance provided will
172
     * be considered the response to return.
173
     *
174
     * Alternately, the middleware may return a response instance.
175
     *
176
     * Often, middleware will `return $out();`, with the assumption that a
177
     * later middleware will return a response.
178
     *
179
     * @param ServerRequestInterface $request
180
     * @param ResponseInterface      $response
181
     * @param null|callable          $out
182
     *
183
     * @return null|ResponseInterface
184
     */
185
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $out = null)
186
    {
187
        try {
188
            $this->purgeExpiredRoutes();
189
190
            $urlNodesCacheItem = $this->cachePool->getItem('splashUrlNodes');
191
            if (!$urlNodesCacheItem->isHit()) {
192
                // No value in cache, let's get the URL nodes
193
                $urlsList = $this->getSplashActionsList();
194
                $urlNodes = $this->generateUrlNode($urlsList);
195
                $urlNodesCacheItem->set($urlNodes);
196
                $this->cachePool->save($urlNodesCacheItem);
197
            }
198
199
            $urlNodes = $urlNodesCacheItem->get();
200
            /* @var $urlNodes SplashUrlNode */
201
202
            $request_path = $request->getUri()->getPath();
203
204
            $pos = strpos($request_path, $this->rootUrl);
205
            if ($pos === false) {
206
                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.'"');
207
            }
208
209
            $tailing_url = substr($request_path, $pos + strlen($this->rootUrl));
210
211
            $splashRoute = $urlNodes->walk($tailing_url, $request);
212
213
            if ($splashRoute === null) {
214
                // No route found. Let's try variants with or without trailing / if we are in a GET.
215
                if ($request->getMethod() === 'GET') {
216
                    // If there is a trailing /, let's remove it and retry
217
                    if (strrpos($tailing_url, '/') === strlen($tailing_url) - 1) {
218
                        $url = substr($tailing_url, 0, -1);
219
                        $splashRoute = $urlNodes->walk($url, $request);
220
                    } else {
221
                        $url = $tailing_url.'/';
222
                        $splashRoute = $urlNodes->walk($url, $request);
223
                    }
224
225
                    if ($splashRoute !== null) {
226
                        // If a route does match, let's make a redirect.
227
                        return new RedirectResponse($this->rootUrl.$url);
228
                    }
229
                }
230
231
                $this->log->debug('Found no route for URL {url}.', [
232
                    'url' => $request_path,
233
                ]);
234
235
                // No route found, let's pass control to the next middleware.
236
                if ($out !== null) {
237
                    return $out($request, $response);
238
                } else {
239
                    throw PageNotFoundException::create($tailing_url);
240
                }
241
            }
242
243
            // Is the route still valid according to the cache?
244
            if (!$splashRoute->isCacheValid()) {
245
                // The route is invalid! Let's purge the cache and retry!
246
                $this->purgeUrlsCache();
247
                return $this($request, $response, $out);
248
            }
249
250
251
            $controller = $this->container->get($splashRoute->getControllerInstanceName());
252
            $action = $splashRoute->getMethodName();
253
254
            $this->log->debug('Routing URL {url} to controller instance {controller} and action {action}', [
255
                'url' => $request_path,
256
                'controller' => $splashRoute->getControllerInstanceName(),
257
                'action' => $action,
258
            ]);
259
260
261
            $filters = $splashRoute->getFilters();
262
263
264
265
            $middlewareCaller = function(ServerRequestInterface $request, ResponseInterface $response) use ($controller, $action, $splashRoute, $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...
266
                // Let's recreate a new context object (because request can be modified by the filters)
267
                $context = new SplashRequestContext($request);
268
                $context->setUrlParameters($splashRoute->getFilledParameters());
269
                // Let's pass everything to the controller:
270
                $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...
271
272
273
274
                $response = SplashUtils::buildControllerResponse(
275
                    function () use ($controller, $action, $args) {
276
                        return $controller->$action(...$args);
277
                    },
278
                    $this->mode,
279
                    $this->debug
280
                );
281
282
                return $response;
283
            };
284
285
            // Apply filters
286
            for ($i = count($filters) - 1; $i >= 0; --$i) {
287
                $filter = $filters[$i];
288
                $middlewareCaller = function(ServerRequestInterface $request, ResponseInterface $response) use ($middlewareCaller, $filter) {
289
                    return $filter($request, $response, $middlewareCaller, $this->container);
290
                };
291
            }
292
293
294
            $response = $middlewareCaller($request, $response);
295
296
            return $response;
297
        } catch (BadRequestException $e) {
298
            if ($this->http400Handler !== null) {
299
                return $this->http400Handler->badRequest($e, $request);
300
            } else {
301
                throw $e;
302
            }
303
        } catch (PageNotFoundException $e) {
304
            if ($this->http404Handler !== null) {
305
                return $this->http404Handler->pageNotFound($request);
306
            } else {
307
                throw $e;
308
            }
309
        } catch (\Throwable $t) {
310
            if ($this->http500Handler !== null) {
311
                return $this->http500Handler->serverError($t, $request);
312
            } else {
313
                throw $t;
314
            }
315
        }
316
    }
317
318
    /**
319
     * Purges the cache if one of the url providers tells us to.
320
     */
321
    private function purgeExpiredRoutes()
322
    {
323
        $expireTag = '';
324
        foreach ($this->routeProviders as $routeProvider) {
325
            /* @var $routeProvider UrlProviderInterface */
326
            $expireTag .= $routeProvider->getExpirationTag();
327
        }
328
329
        $value = md5($expireTag);
330
331
        $urlNodesCacheItem = $this->cachePool->getItem('splashExpireTag');
332
333
        if ($urlNodesCacheItem->isHit() && $urlNodesCacheItem->get() === $value) {
334
            return;
335
        }
336
337
        $this->purgeUrlsCache();
338
339
        $urlNodesCacheItem->set($value);
340
        $this->cachePool->save($urlNodesCacheItem);
341
    }
342
343
344
    /**
345
     * Returns the list of all SplashActions.
346
     * This call is LONG and should be cached.
347
     *
348
     * @return array<SplashAction>
349
     */
350
    public function getSplashActionsList()
351
    {
352
        $urls = array();
353
354
        foreach ($this->routeProviders as $routeProvider) {
355
            /* @var $routeProvider UrlProviderInterface */
356
            $tmpUrlList = $routeProvider->getUrlsList(null);
357
            $urls = array_merge($urls, $tmpUrlList);
358
        }
359
360
        return $urls;
361
    }
362
363
    /**
364
     * Generates the URLNodes from the list of URLS.
365
     * URLNodes are a very efficient way to know whether we can access our page or not.
366
     *
367
     * @param array<SplashAction> $urlsList
368
     *
369
     * @return SplashUrlNode
370
     */
371
    private function generateUrlNode($urlsList)
372
    {
373
        $urlNode = new SplashUrlNode();
374
        foreach ($urlsList as $splashAction) {
375
            $urlNode->registerCallback($splashAction);
376
        }
377
378
        return $urlNode;
379
    }
380
381
    /**
382
     * Purges the urls cache.
383
     */
384
    public function purgeUrlsCache()
385
    {
386
        $this->cachePool->deleteItem('splashUrlNodes');
387
    }
388
}
389