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

SplashDefaultRouter::__construct()   A

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 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