SplashDefaultRouter::__construct()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
c 0
b 0
f 0
rs 9.4285
cc 3
eloc 9
nc 4
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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