Test Failed
Push — master ( a25a4c...498f08 )
by Divine Niiquaye
13:17
created

RoutingExtension::booRoutes()   D

Complexity

Conditions 23
Paths 23

Size

Total Lines 79
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 23
eloc 42
c 0
b 0
f 0
nc 23
nop 2
dl 0
loc 79
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of DivineNii opensource projects.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 DivineNii (https://divinenii.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Rade\DI\Extensions;
19
20
use Biurad\Http\Middlewares\ErrorHandlerMiddleware;
21
use Flight\Routing\Annotation\Listener;
22
use Flight\Routing\Interfaces\RouteMatcherInterface;
23
use Flight\Routing\Middlewares\PathMiddleware;
24
use Flight\Routing\RouteCollection;
25
use Flight\Routing\Router;
26
use Flight\Routing\Route;
27
use Nette\Utils\Arrays;
28
use Psr\Http\Server\MiddlewareInterface;
29
use Psr\Http\Server\RequestHandlerInterface;
30
use Rade\DI\AbstractContainer;
31
use Rade\DI\Builder\PhpLiteral;
32
use Rade\DI\Container;
33
use Rade\DI\ContainerBuilder;
34
use Rade\DI\Definition;
35
use Rade\DI\Definitions\Reference;
36
use Rade\DI\Definitions\Statement;
37
use Rade\DI\Exceptions\ServiceCreationException;
38
use Rade\DI\Services\AliasedInterface;
39
use Rade\DI\Services\DependenciesInterface;
40
use Rade\Handler\RouteHandler;
41
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
42
use Symfony\Component\Config\Definition\ConfigurationInterface;
43
44
/**
45
 * Flight Routing Extension. (Recommend to be used with AppBuilder).
46
 *
47
 * @author Divine Niiquaye Ibok <[email protected]>
48
 */
49
class RoutingExtension implements AliasedInterface, BootExtensionInterface, ConfigurationInterface, DependenciesInterface, ExtensionInterface
50
{
51
    protected const ROUTE_DATA_TO_METHOD = [
52
        'name' => 'bind',
53
        'prefix' => 'prefix',
54
        'hosts' => 'domain',
55
        'schemes' => 'scheme',
56
        'methods' => 'method',
57
        'defaults' => 'defaults',
58
        'arguments' => 'arguments',
59
        'middlewares' => 'piped',
60
        'patterns' => 'asserts',
61
        'namespace' => 'namespace',
62
    ];
63
64
    /** @var array<int,object> */
65
    private array $middlewares = [];
66
67
    private ?string $routeNamespace;
68
69
    /**
70
     * {@inheritdoc}
71
     */
72
    public function dependencies(): array
73
    {
74
        return [HttpGalaxyExtension::class];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array(Rade\DI\Ext...GalaxyExtension::class) returns the type array<integer,string> which is incompatible with the return type mandated by Rade\DI\Services\Depende...terface::dependencies() of Rade\DI\Services\ServiceProviderInterface[].

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function getAlias(): string
81
    {
82
        return 'routing';
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88
    public function getConfigTreeBuilder(): TreeBuilder
89
    {
90
        $treeBuilder = new TreeBuilder($this->getAlias());
91
92
        $treeBuilder->getRootNode()
93
            ->addDefaultsIfNotSet()
0 ignored issues
show
Bug introduced by
The method addDefaultsIfNotSet() does not exist on Symfony\Component\Config...\Builder\NodeDefinition. It seems like you code against a sub-type of Symfony\Component\Config...\Builder\NodeDefinition such as Symfony\Component\Config...der\ArrayNodeDefinition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

93
            ->/** @scrutinizer ignore-call */ addDefaultsIfNotSet()
Loading history...
94
            ->children()
95
                ->booleanNode('redirect_permanent')->defaultFalse()->end()
96
                ->booleanNode('keep_request_method')->defaultFalse()->end()
97
                ->booleanNode('response_error')->defaultValue(null)->end()
98
                ->booleanNode('resolve_route_paths')->defaultTrue()->end()
99
                ->scalarNode('namespace')->defaultValue(null)->end()
100
                ->scalarNode('routes_handler')->end()
101
                ->scalarNode('cache')->end()
102
                ->arrayNode('middlewares')
103
                    ->scalarPrototype()->end()
104
                ->end()
105
                ->arrayNode('pipes')
106
                    ->normalizeKeys(false)
107
                    ->defaultValue([])
108
                    ->beforeNormalization()
109
                        ->ifTrue(fn ($v) => !\is_array($v) || \array_is_list($v))
110
                        ->thenInvalid('Expected patterns values to be an associate array of string keys mapping to mixed values.')
111
                    ->end()
112
                    ->prototype('variable')->end()
113
                ->end()
114
                ->arrayNode('routes')
115
                    ->arrayPrototype()
116
                        ->addDefaultsIfNotSet()
117
                        ->children()
118
                            ->scalarNode('name')->defaultValue(null)->end()
119
                            ->scalarNode('path')->isRequired()->end()
120
                            ->scalarNode('prefix')->defaultValue(null)->end()
121
                            ->scalarNode('to')->defaultValue(null)->end()
122
                            ->scalarNode('namespace')->defaultValue(null)->end()
123
                            ->booleanNode('debug')->defaultValue(null)->end()
124
                            ->arrayNode('methods')
125
                                ->beforeNormalization()
126
                                    ->ifString()
127
                                    ->then(fn (string $v): array => [$v])
128
                                ->end()
129
                                ->defaultValue(Route::DEFAULT_METHODS)
130
                                ->prototype('scalar')->end()
131
                            ->end()
132
                            ->arrayNode('schemes')
133
                                ->beforeNormalization()
134
                                    ->ifString()
135
                                    ->then(fn (string $v): array => [$v])
136
                                ->end()
137
                                ->prototype('scalar')->defaultValue([])->end()
138
                            ->end()
139
                            ->arrayNode('hosts')
140
                                ->beforeNormalization()
141
                                    ->ifString()
142
                                    ->then(fn (string $v): array => [$v])
143
                                ->end()
144
                                ->prototype('scalar')->defaultValue([])->end()
145
                            ->end()
146
                            ->arrayNode('middlewares')
147
                                ->beforeNormalization()
148
                                    ->ifString()
149
                                    ->then(fn (string $v): array => [$v])
150
                                ->end()
151
                                ->prototype('scalar')->defaultValue([])->end()
152
                            ->end()
153
                            ->arrayNode('patterns')
154
                                ->normalizeKeys(false)
155
                                ->defaultValue([])
156
                                ->beforeNormalization()
157
                                    ->ifTrue(fn ($v) => !\is_array($v) || \array_is_list($v))
158
                                    ->thenInvalid('Expected patterns values to be an associate array of string keys mapping to mixed values.')
159
                                    ->ifArray()
160
                                    ->then(fn (array $v): array => $v[0])
161
                                ->end()
162
                                ->prototype('variable')->end()
163
                            ->end()
164
                            ->arrayNode('defaults')
165
                                ->normalizeKeys(false)
166
                                ->defaultValue([])
167
                                ->beforeNormalization()
168
                                    ->ifTrue(fn ($v) => !\is_array($v) || \array_is_list($v))
169
                                    ->thenInvalid('Expected defaults values to be an associate array of string keys mapping to mixed values.')
170
                                    ->ifArray()
171
                                    ->then(fn (array $v): array => $v[0])
172
                                ->end()
173
                                ->prototype('variable')->end()
174
                            ->end()
175
                            ->arrayNode('arguments')
176
                                ->normalizeKeys(false)
177
                                ->defaultValue([])
178
                                ->beforeNormalization()
179
                                    ->ifTrue(fn ($v) => !\is_array($v) || \array_is_list($v))
180
                                    ->thenInvalid('Expected arguments values to be an associate array of string keys mapping to mixed values.')
181
                                    ->ifArray()
182
                                    ->then(fn (array $v): array => $v[0])
183
                                ->end()
184
                                ->prototype('variable')->end()
185
                            ->end()
186
                        ->end()
187
                    ->end()
188
                ->end()
189
            ->end()
190
        ;
191
192
        return $treeBuilder;
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198
    public function register(AbstractContainer $container, array $configs = []): void
199
    {
200
        $routeHandler = $configs['routes_handler'] ?? RouteHandler::class;
201
        $this->routeNamespace = $configs['namespace'] ?? null;
202
203
        if ($container->has($routeHandler)) {
204
            $container->alias(RequestHandlerInterface::class, $routeHandler);
205
        } else {
206
            $container->set(RequestHandlerInterface::class, new Definition($routeHandler));
207
        }
208
209
        if ($container->hasExtension(AnnotationExtension::class)) {
210
            $container->autowire('router.annotation_listener', new Definition(Listener::class, [new Statement(RouteCollection::class)]));
211
        }
212
213
        if (!$container->has('http.router')) {
214
            $container->set('http.router', new Definition(Router::class))->autowire([Router::class, RouteMatcherInterface::class]);
215
        }
216
217
        $pipesMiddleware = \array_map(static function (array $middlewares) use ($container): array {
218
            foreach ($middlewares as &$middleware) {
219
                if ($container->has($middleware)) {
220
                    $middleware = new Reference($middleware);
221
222
                    continue;
223
                }
224
225
                $middleware = new Statement($middleware);
226
            }
227
228
            return $middlewares;
229
        }, $configs['pipes']);
230
        $this->middlewares = \array_map(static function (string $middleware) use ($container): object {
231
            if ($container->has($middleware)) {
232
                return new Reference($middleware);
233
            }
234
235
            return new Statement($middleware);
236
        }, $configs['middlewares']);
237
238
        $router = $container->definition('http.router');
239
        $this->middlewares[] = new Reference('http.middleware.headers');
240
241
        if ($configs['resolve_route_paths']) {
242
            $this->middlewares[] = new Statement(PathMiddleware::class, [$configs['redirect_permanent'], $configs['keep_request_method']]);
243
        }
244
245
        if ($container->has('http.middleware.cookie')) {
246
            $this->middlewares[] = new Reference('http.middleware.cookie');
247
        }
248
249
        if ($container->has('http.middleware.policies')) {
250
            $this->middlewares[] = new Reference('http.middleware.policies');
251
        }
252
253
        if ($container->has('http.middleware.cors')) {
254
            $this->middlewares[] = new Reference('http.middleware.cors');
255
        }
256
257
        if (isset($configs['response_error'])) {
258
            $this->middlewares[] = new Statement(ErrorHandlerMiddleware::class, [$configs['response_error']]);
259
        }
260
261
        if ($container->has('http.middleware.cache')) {
262
            $this->middlewares[] = new Reference('http.middleware.cache');
263
        }
264
265
        if ($router instanceof Router) {
0 ignored issues
show
introduced by
$router is never a sub-type of Flight\Routing\Router.
Loading history...
266
            if (!$container instanceof Container) {
267
                throw new ServiceCreationException(\sprintf('Constructing a "%s" instance requires non-builder container.', Router::class));
268
            }
269
270
            foreach ($pipesMiddleware as $middlewareId => $middlewares) {
271
                $router->pipes($middlewareId, ...$container->getResolver()->resolveArguments($middlewares));
272
            }
273
        } else {
274
            foreach ($pipesMiddleware as $middlewareId => $middlewares) {
275
                $router->bind('pipes', [$middlewareId, $middlewares]);
276
            }
277
278
            if ($configs['cache']) {
279
                $router->arg(1, $container->has($configs['cache']) ? new Reference($configs['cache']) : $configs['cache']);
280
            }
281
        }
282
283
        $container->parameters['routes'] = \array_merge($configs['routes'] ?? [], $container->parameters['routes'] ?? []);
284
    }
285
286
    /**
287
     * {@inheritdoc}
288
     */
289
    public function boot(AbstractContainer $container): void
290
    {
291
        [$collection, $groups] = $this->booRoutes($container, $this->routeNamespace);
292
293
        $routes = $container->findBy(RouteCollection::class, static function (string $routesId) use ($container, $groups) {
294
            if (!empty($collection = $groups[$routesId] ?? [])) {
295
                $grouped = $container->definition($routesId);
296
297
                if ($grouped instanceof RouteCollection) {
0 ignored issues
show
introduced by
$grouped is never a sub-type of Flight\Routing\RouteCollection.
Loading history...
298
                    if (!$container instanceof Container) {
299
                        throw new ServiceCreationException(\sprintf('Constructing a "%s" instance requires non-builder container.', RouteCollection::class));
300
                    }
301
302
                    return $grouped->prototype($collection)->end();
303
                }
304
305
                $grouped->bind('prototype', [$collection]);
306
            }
307
308
            return new Reference($routesId);
309
        });
310
        $middlewares = $container->findBy(MiddlewareInterface::class, function (string $middlewareId) use ($container) {
311
            $middleware = $container->definition($middlewareId);
312
313
            if ($middleware instanceof MiddlewareInterface) {
314
                if (!$container instanceof Container) {
315
                    throw new ServiceCreationException(\sprintf('Constructing a "%s" instance requires non-builder container.', MiddlewareInterface::class));
316
                }
317
318
                return $middleware;
319
            }
320
321
            return new Reference($middlewareId);
322
        });
323
324
        $router = $container->definition('http.router');
325
        $middlewares = \array_merge($middlewares, $this->middlewares);
326
327
        if ($router instanceof Router) {
0 ignored issues
show
introduced by
$router is never a sub-type of Flight\Routing\Router.
Loading history...
328
            if (!$container instanceof Container) {
329
                throw new ServiceCreationException(\sprintf('Constructing a "%s" instance requires non-builder container.', Router::class));
330
            }
331
332
            $router->pipe(...$container->getResolver()->resolveArguments($middlewares));
333
334
            if (!empty($collection)) {
335
                $router->getCollection()->routes($collection[0]);
336
            }
337
338
            foreach ($routes as $group) {
339
                $router->getCollection()->populate($group instanceof Reference ? $container->get((string) $group) : $group, true);
340
                $container->removeDefinition((string) $group);
341
            }
342
343
            $container->removeType(RouteCollection::class);
344
        } else {
345
            if ($container instanceof Container) {
346
                throw new ServiceCreationException(\sprintf('Constructing a "%s" instance requires a builder container.', Router::class));
347
            }
348
349
            $router->bind('pipe', [$middlewares]);
350
            $groupedCollection = 'function (\Flight\Routing\RouteCollection $collection) {';
351
352
            if (!empty($collection)) {
353
                $groupedCollection .= '$collection->routes(\'??\');';
354
            }
355
356
            foreach ($routes as $group) {
357
                $groupedCollection .= '$collection->populate(\'??\', true);';
358
                $collection[] = $group;
359
            }
360
361
            $router->bind('setCollection', new PhpLiteral($groupedCollection . '};', $collection));
362
        }
363
364
        unset($container->parameters['routes']);
365
    }
366
367
    public function booRoutes(AbstractContainer $container, ?string $routeNamespace): array
368
    {
369
        $collection = $groups = [];
370
371
        foreach ($container->parameters['routes'] ?? [] as $routeData) {
372
            if (isset($routeData['debug']) && $container->parameters['debug'] !== $routeData['debug']) {
373
                continue;
374
            }
375
376
            if ('@' === $routeData['path'][0]) {
377
                $routesId = \substr($routeData['path'], 1);
378
379
                if (null !== $routeData['to'] ?? $routeData['run'] ?? null) {
380
                    throw new ServiceCreationException(\sprintf('Route declared for collection with id "%s", must not include a handler.', $routesId));
381
                }
382
383
                if (null !== $routeData['name'] ?? null) {
384
                    throw new ServiceCreationException(\sprintf('Route declared for collection with id "%s", must not include a name.', $routesId));
385
                }
386
387
                unset($routeData['to'], $routeData['run'], $routeData['path'], $routeData['name']);
388
389
                foreach ($routeData as $key => $value) {
390
                    if (!empty($value)) {
391
                        if ($container instanceof Container) {
392
                            $value = \is_array($value) && !\array_is_list($value) ? [$value] : $value;
393
                        }
394
395
                        $groups[$routesId][self::ROUTE_DATA_TO_METHOD[$key] ?? $key] = $value;
396
                    }
397
                }
398
399
                continue;
400
            }
401
402
            /** @var array<int,mixed> $routeArgs */
403
            $routeArgs = [
404
                Arrays::pick($routeData, 'path'),
405
                Arrays::pick($routeData, isset($routeData['methods']) ? 'methods' : 'method', []),
406
                Arrays::pick($routeData, isset($routeData['to']) ? 'to' : 'run', null),
407
            ];
408
409
            $routeNamespace = (string) $routeNamespace . ($routeData['namespace'] ?? '');
410
            unset($routeData['namespace']);
411
412
            if ($container instanceof ContainerBuilder) {
413
                $createRoute = Route::class . '::to(\'??\',\'??\', \'??\')';
414
415
                if (!empty($routeNamespace)) {
416
                    $createRoute .= '->namespace(\'??\')';
417
                    $routeArgs[] = $routeNamespace;
418
                }
419
420
                foreach ($routeData as $key => $value) {
421
                    if (!empty($value)) {
422
                        $createRoute .= '->' . (self::ROUTE_DATA_TO_METHOD[$key] ?? $key) . \sprintf("(%s'??')", \is_array($value) && \array_is_list($value) ? '...' : '');
423
                        $routeArgs[] = $value;
424
                    }
425
                }
426
427
                $createRoute = new PhpLiteral($createRoute . ';', $routeArgs);
428
            } else {
429
                $createRoute = Route::to($routeArgs[0], $routeArgs[1], $routeArgs[2]);
430
431
                if (!empty($routeNamespace)) {
432
                    $createRoute->namespace($routeNamespace);
433
                }
434
435
                foreach ($routeData as $key => $value) {
436
                    if (!empty($value)) {
437
                        $container->getResolver()->resolveCallable([$createRoute, self::ROUTE_DATA_TO_METHOD[$key] ?? $key], [$value]);
438
                    }
439
                }
440
            }
441
442
            $collection[0][] = $createRoute;
443
        }
444
445
        return [$collection, $groups];
446
    }
447
}
448