SplashDefaultRouter::generateUrlNode()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
c 0
b 0
f 0
rs 9.6666
cc 2
eloc 5
nc 2
nop 1
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, string $mode = SplashUtils::MODE_STRICT, bool $debug = true, string $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 = rtrim($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
            return $this->route($request, $response, $out);
189
        } catch (BadRequestException $e) {
190
            if ($this->http400Handler !== null) {
191
                return $this->http400Handler->badRequest($e, $request);
192
            } else {
193
                throw $e;
194
            }
195
        } catch (PageNotFoundException $e) {
196
            if ($this->http404Handler !== null) {
197
                return $this->http404Handler->pageNotFound($request);
198
            } else {
199
                throw $e;
200
            }
201
        } catch (\Throwable $t) {
202
            if ($this->http500Handler !== null) {
203
                return $this->http500Handler->serverError($t, $request);
204
            } else {
205
                throw $t;
206
            }
207
        }
208
    }
209
210
    /**
211
     * @param ServerRequestInterface $request
212
     * @param ResponseInterface      $response
213
     * @param callable|null          $out
214
     *
215
     * @return ResponseInterface
216
     */
217
    private function route(ServerRequestInterface $request, ResponseInterface $response, callable $out = null, $retry = false) : ResponseInterface
218
    {
219
        $this->purgeExpiredRoutes();
220
221
        $urlNodesCacheItem = $this->cachePool->getItem('splashUrlNodes');
222
        if (!$urlNodesCacheItem->isHit()) {
223
            // No value in cache, let's get the URL nodes
224
            $urlsList = $this->getSplashActionsList();
225
            $urlNodes = $this->generateUrlNode($urlsList);
226
            $urlNodesCacheItem->set($urlNodes);
227
            $this->cachePool->save($urlNodesCacheItem);
228
        }
229
230
        $urlNodes = $urlNodesCacheItem->get();
231
        /* @var $urlNodes SplashUrlNode */
232
233
        $request_path = $request->getUri()->getPath();
234
235
        $pos = strpos($request_path, $this->rootUrl);
236
        if ($pos === false) {
237
            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.'"');
238
        }
239
240
        $tailing_url = substr($request_path, $pos + strlen($this->rootUrl));
241
        $tailing_url = urldecode($tailing_url);
242
        $splashRoute = $urlNodes->walk($tailing_url, $request);
243
244
        if ($splashRoute === null) {
245
            // No route found. Let's try variants with or without trailing / if we are in a GET.
246
            if ($request->getMethod() === 'GET') {
247
                // If there is a trailing /, let's remove it and retry
248
                if (strrpos($tailing_url, '/') === strlen($tailing_url) - 1) {
249
                    $url = substr($tailing_url, 0, -1);
250
                    $splashRoute = $urlNodes->walk($url, $request);
251
                } else {
252
                    $url = $tailing_url.'/';
253
                    $splashRoute = $urlNodes->walk($url, $request);
254
                }
255
256
                if ($splashRoute !== null) {
257
                    // If a route does match, let's make a redirect.
258
                    return new RedirectResponse($this->rootUrl.$url);
259
                }
260
            }
261
262
            $this->log->debug('Found no route for URL {url}.', [
263
                'url' => $request_path,
264
            ]);
265
266
            if ($this->debug === false || $retry === true) {
267
                // No route found, let's pass control to the next middleware.
268
                if ($out !== null) {
269
                    return $out($request, $response);
270
                } else {
271
                    throw PageNotFoundException::create($tailing_url);
272
                }
273
            } else {
274
                // We have a 404, but we are in debug mode and have not retried yet...
275
                // Let's purge the cache and retry!
276
                $this->purgeUrlsCache();
277
278
                return $this->route($request, $response, $out, true);
279
            }
280
        }
281
282
        // Is the route still valid according to the cache?
283
        if (!$splashRoute->isCacheValid()) {
284
            // The route is invalid! Let's purge the cache and retry!
285
            $this->purgeUrlsCache();
286
287
            return $this($request, $response, $out);
288
        }
289
290
        $controller = $this->container->get($splashRoute->getControllerInstanceName());
291
        $action = $splashRoute->getMethodName();
292
293
        $this->log->debug('Routing URL {url} to controller instance {controller} and action {action}', [
294
            'url' => $request_path,
295
            'controller' => $splashRoute->getControllerInstanceName(),
296
            'action' => $action,
297
        ]);
298
299
        $filters = $splashRoute->getFilters();
300
301
        $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...
302
            // Let's recreate a new context object (because request can be modified by the filters)
303
            $context = new SplashRequestContext($request);
304
            $context->setUrlParameters($splashRoute->getFilledParameters());
305
            // Let's pass everything to the controller:
306
            $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...
307
308
            try {
309
                $response = SplashUtils::buildControllerResponse(
310
                    function () use ($controller, $action, $args) {
311
                        return $controller->$action(...$args);
312
                    },
313
                    $this->mode,
314
                    $this->debug
315
                );
316
            } catch (SplashException $e) {
317
                throw new SplashException($e->getMessage(). ' (in '.$splashRoute->getControllerInstanceName().'->'.$splashRoute->getMethodName().')', $e->getCode(), $e);
318
            }
319
            return $response;
320
        };
321
322
        // Apply filters
323
        for ($i = count($filters) - 1; $i >= 0; --$i) {
324
            $filter = $filters[$i];
325
            $middlewareCaller = function (ServerRequestInterface $request, ResponseInterface $response) use ($middlewareCaller, $filter) {
326
                return $filter($request, $response, $middlewareCaller, $this->container);
327
            };
328
        }
329
330
        $response = $middlewareCaller($request, $response);
331
332
        return $response;
333
    }
334
335
    /**
336
     * Purges the cache if one of the url providers tells us to.
337
     */
338
    private function purgeExpiredRoutes()
339
    {
340
        $expireTag = '';
341
        foreach ($this->routeProviders as $routeProvider) {
342
            /* @var $routeProvider UrlProviderInterface */
343
            $expireTag .= $routeProvider->getExpirationTag();
344
        }
345
346
        $value = md5($expireTag);
347
348
        $urlNodesCacheItem = $this->cachePool->getItem('splashExpireTag');
349
350
        if ($urlNodesCacheItem->isHit() && $urlNodesCacheItem->get() === $value) {
351
            return;
352
        }
353
354
        $this->purgeUrlsCache();
355
356
        $urlNodesCacheItem->set($value);
357
        $this->cachePool->save($urlNodesCacheItem);
358
    }
359
360
    /**
361
     * Returns the list of all SplashActions.
362
     * This call is LONG and should be cached.
363
     *
364
     * @return array<SplashAction>
365
     */
366
    public function getSplashActionsList()
367
    {
368
        $urls = array();
369
370
        foreach ($this->routeProviders as $routeProvider) {
371
            /* @var $routeProvider UrlProviderInterface */
372
            $tmpUrlList = $routeProvider->getUrlsList(null);
373
            $urls = array_merge($urls, $tmpUrlList);
374
        }
375
376
        return $urls;
377
    }
378
379
    /**
380
     * Generates the URLNodes from the list of URLS.
381
     * URLNodes are a very efficient way to know whether we can access our page or not.
382
     *
383
     * @param array<SplashAction> $urlsList
384
     *
385
     * @return SplashUrlNode
386
     */
387
    private function generateUrlNode($urlsList)
388
    {
389
        $urlNode = new SplashUrlNode();
390
        foreach ($urlsList as $splashAction) {
391
            $urlNode->registerCallback($splashAction);
392
        }
393
394
        return $urlNode;
395
    }
396
397
    /**
398
     * Purges the urls cache.
399
     */
400
    public function purgeUrlsCache()
401
    {
402
        $this->cachePool->deleteItem('splashUrlNodes');
403
    }
404
}
405