Test Failed
Push — master ( dc68d1...1f5199 )
by Mathieu
02:31
created

GenericRoute   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 539
Duplicated Lines 3.71 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 10
Bugs 0 Features 2
Metric Value
wmc 55
c 10
b 0
f 2
lcom 1
cbo 6
dl 20
loc 539
rs 6.8

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A setDependencies() 0 10 2
B pathResolvable() 0 17 5
A __invoke() 0 17 2
A resolveLatestObjectRoute() 0 18 2
F resolveTemplateContextObject() 0 88 16
A createTemplate() 0 9 1
A createRouteObject() 0 6 1
A setObjectRouteClass() 0 12 2
A objectRouteClass() 0 4 1
A loadContextObject() 0 15 2
A loadObjectRouteFromPath() 0 21 2
A getLatestObjectPathHistory() 0 21 1
A setPath() 0 6 1
A setModelFactory() 0 6 1
A setCollectionLoader() 0 6 1
B setLocale() 0 23 4
A path() 0 4 1
A modelFactory() 10 10 2
A collectionLoader() 10 10 2
A cacheEnabled() 0 5 2
A cacheTtl() 0 5 2
A cacheIdent() 0 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like GenericRoute often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GenericRoute, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Charcoal\Cms\Route;
4
5
use RuntimeException;
6
use InvalidArgumentException;
7
8
// From Pimple
9
use Pimple\Container;
10
11
// From PSR-7
12
use Psr\Http\Message\RequestInterface;
13
use Psr\Http\Message\ResponseInterface;
14
15
// From 'charcoal-app'
16
use Charcoal\App\Route\TemplateRoute;
17
18
// From 'charcoal-cms'
19
use Charcoal\Cms\TemplateableInterface;
20
21
// From 'charcoal-factory'
22
use Charcoal\Factory\FactoryInterface;
23
24
// From 'charcoal-core'
25
use Charcoal\Model\ModelInterface;
26
use Charcoal\Loader\CollectionLoader;
27
28
// From 'charcoal-translator'
29
use Charcoal\Translator\TranslatorAwareTrait;
30
31
// From 'charcoal-object'
32
use Charcoal\Object\ObjectRoute;
33
use Charcoal\Object\ObjectRouteInterface;
34
use Charcoal\Object\RoutableInterface;
35
36
/**
37
 * Generic Object Route Handler
38
 *
39
 * Uses implementations of {@see \Charcoal\Object\ObjectRouteInterface}
40
 * to match routes for catch-all routing patterns.
41
 */
42
class GenericRoute extends TemplateRoute
43
{
44
    use TranslatorAwareTrait;
45
46
    /**
47
     * The URI path.
48
     *
49
     * @var string
50
     */
51
    private $path;
52
53
    /**
54
     * The object route.
55
     *
56
     * @var ObjectRouteInterface
57
     */
58
    private $objectRoute;
59
60
    /**
61
     * The target object of the {@see GenericRoute Chainable::$objectRoute}.
62
     *
63
     * @var ModelInterface|RoutableInterface
64
     */
65
    private $contextObject;
66
67
    /**
68
     * Store the factory instance for the current class.
69
     *
70
     * @var FactoryInterface
71
     */
72
    private $modelFactory;
73
74
    /**
75
     * Store the collection loader for the current class.
76
     *
77
     * @var CollectionLoader
78
     */
79
    private $collectionLoader;
80
81
    /**
82
     * The class name of the object route model.
83
     *
84
     * Must be a fully-qualified PHP namespace and an implementation of
85
     * {@see \Charcoal\Object\ObjectRouteInterface}. Used by the model factory.
86
     *
87
     * @var string
88
     */
89
    protected $objectRouteClass = ObjectRoute::class;
90
91
    /**
92
     * Store the available templates.
93
     *
94
     * @var array
95
     */
96
    protected $availableTemplates = [];
97
98
    /**
99
     * Returns new template route object.
100
     *
101
     * @param array $data Class depdendencies.
102
     */
103
    public function __construct(array $data)
104
    {
105
        parent::__construct($data);
106
107
        $this->setPath(ltrim($data['path'], '/'));
108
    }
109
110
    /**
111
     * Inject dependencies from a DI Container.
112
     *
113
     * @param  Container $container A dependencies container instance.
114
     * @return void
115
     */
116
    public function setDependencies(Container $container)
117
    {
118
        $this->setTranslator($container['translator']);
119
        $this->setModelFactory($container['model/factory']);
120
        $this->setCollectionLoader($container['model/collection/loader']);
121
122
        if (isset($container['config']['templates'])) {
123
            $this->availableTemplates = $container['config']['templates'];
124
        }
125
    }
126
127
    /**
128
     * Determine if the URI path resolves to an object.
129
     *
130
     * @param  Container $container A DI (Pimple) container.
131
     * @return boolean
132
     */
133
    public function pathResolvable(Container $container)
134
    {
135
        $this->setDependencies($container);
136
137
        $object = $this->loadObjectRouteFromPath();
138
        if (!$object->id()) {
139
            return false;
140
        }
141
142
        $contextObject = $this->loadContextObject();
143
144
        if (!$contextObject || !$contextObject->id()) {
145
            return false;
146
        }
147
148
        return (!!$contextObject->active() && !!$contextObject->isActiveRoute());
149
    }
150
151
    /**
152
     * Resolve the dynamic route.
153
     *
154
     * @param  Container         $container A DI (Pimple) container.
155
     * @param  RequestInterface  $request   A PSR-7 compatible Request instance.
156
     * @param  ResponseInterface $response  A PSR-7 compatible Response instance.
157
     * @return ResponseInterface
158
     */
159
    public function __invoke(
160
        Container $container,
161
        RequestInterface $request,
162
        ResponseInterface $response
163
    ) {
164
        $response = $this->resolveLatestObjectRoute($request, $response);
165
166
        if (!$response->isRedirect()) {
167
            $this->resolveTemplateContextObject();
168
169
            $templateContent = $this->templateContent($container, $request);
170
171
            $response->write($templateContent);
172
        }
173
174
        return $response;
175
    }
176
177
    /**
178
     * @param  RequestInterface  $request  A PSR-7 compatible Request instance.
179
     * @param  ResponseInterface $response A PSR-7 compatible Response instance.
180
     * @return ResponseInterface
181
     */
182
    protected function resolveLatestObjectRoute(
183
        RequestInterface $request,
184
        ResponseInterface $response
185
    ) {
186
        // Current object route
187
        $objectRoute = $this->loadObjectRouteFromPath();
188
189
        // Could be the SAME as current object route
190
        $latest = $this->getLatestObjectPathHistory($objectRoute);
191
192
        // Redirect if latest route is newer
193
        if ($latest->creationDate() > $objectRoute->creationDate()) {
194
            $redirection = $this->parseRedirect($latest->slug(), $request);
195
            $response = $response->withRedirect($redirection, 301);
196
        }
197
198
        return $response;
199
    }
200
201
    /**
202
     * @return self
203
     */
204
    protected function resolveTemplateContextObject()
205
    {
206
        $config = $this->config();
207
208
        $objectRoute = $this->loadObjectRouteFromPath();
209
        $contextObject = $this->loadContextObject();
210
        $translator = $this->translator();
0 ignored issues
show
Unused Code introduced by
$translator is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
211
        $currentLang = $objectRoute->lang();
212
213
        // Set language according to the route's language
214
        $this->setLocale($currentLang);
215
216
        $templateChoice = [];
217
218
        // Templateable Objects have specific methods
219
        if ($contextObject instanceof TemplateableInterface) {
220
            $identProperty = $contextObject->property('template_ident');
221
            $controllerProperty = $contextObject->property('controller_ident');
0 ignored issues
show
Unused Code introduced by
$controllerProperty is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
222
223
            // Methods from TemplateableInterface / Trait
224
            $templateIdent = $contextObject->templateIdent() ?: $objectRoute->routeTemplate();
225
            // Default fallback to routeTemplate
226
            $controllerIdent = $contextObject->controllerIdent() ?: $templateIdent;
227
228
            $templateChoice = $identProperty->choice($templateIdent);
229
        } else {
230
            // Use global templates to verify for custom paths
231
            $templateIdent = $objectRoute->routeTemplate();
232
            $controllerIdent = $templateIdent;
233
            foreach ($this->availableTemplates as $templateKey => $templateData) {
234
                if (!isset($templateData['value'])) {
235
                    $templateData['value'] = $templateKey;
236
                }
237
                if ($templateData['value'] === $templateIdent) {
238
                    $templateChoice = $templateData;
239
                    break;
240
                }
241
            }
242
        }
243
244
        // Template ident defined in template global config
245
        // Check for custom path / controller
246
        if (isset($templateChoice['template'])) {
247
            $templatePath = $templateChoice['template'];
248
            $templateController = $templateChoice['template'];
249
        } else {
250
            $templatePath = $templateIdent;
251
            $templateController = $controllerIdent;
252
        }
253
254
        // Template controller defined in choices, affect it.
255
        if (isset($templateChoice['controller'])) {
256
            $templateController = $templateChoice['controller'];
257
        }
258
259
        $config['template'] = $templatePath;
260
        $config['controller'] = $templateController;
261
262
        // Always be an array
263
        $templateOptions = [];
264
265
        // Custom template options
266
        if (isset($templateChoice['template_options'])) {
267
            $templateOptions = $templateChoice['template_options'];
268
        }
269
270
        // Overwrite from custom object template_options
271
        if ($contextObject instanceof TemplateableInterface) {
272
            if (!empty($contextObject->templateOptions())) {
273
                $templateOptions = $contextObject->templateOptions();
274
            }
275
        }
276
277
        if (isset($templateOptions) && $templateOptions) {
278
            // Not sure what this was about?
279
            $config['template_data'] = array_merge($config['template_data'], $templateOptions);
280
        }
281
282
        // Merge Route options from object-route
283
        $routeOptions = $objectRoute->routeOptions();
284
        if ($routeOptions && count($routeOptions)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $routeOptions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
285
            $config['template_data'] = array_merge($config['template_data'], $routeOptions);
286
        }
287
288
        $this->setConfig($config);
289
290
        return $this;
291
    }
292
293
    /**
294
     * @param  Container        $container A DI (Pimple) container.
295
     * @param  RequestInterface $request   The request to intialize the template with.
296
     * @return string
297
     */
298
    protected function createTemplate(Container $container, RequestInterface $request)
299
    {
300
        $template = parent::createTemplate($container, $request);
301
302
        $contextObject = $this->loadContextObject();
303
        $template->setContextObject($contextObject);
0 ignored issues
show
Bug introduced by
The method setContextObject cannot be called on $template (of type string).

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...
304
305
        return $template;
306
    }
307
308
    /**
309
     * Create a route object.
310
     *
311
     * @return ObjectRouteInterface
312
     */
313
    public function createRouteObject()
314
    {
315
        $route = $this->modelFactory()->create($this->objectRouteClass());
316
317
        return $route;
318
    }
319
320
    /**
321
     * Set the class name of the object route model.
322
     *
323
     * @param  string $className The class name of the object route model.
324
     * @throws InvalidArgumentException If the class name is not a string.
325
     * @return self
326
     */
327
    protected function setObjectRouteClass($className)
328
    {
329
        if (!is_string($className)) {
330
            throw new InvalidArgumentException(
331
                'Route class name must be a string.'
332
            );
333
        }
334
335
        $this->objectRouteClass = $className;
336
337
        return $this;
338
    }
339
340
    /**
341
     * Retrieve the class name of the object route model.
342
     *
343
     * @return string
344
     */
345
    public function objectRouteClass()
346
    {
347
        return $this->objectRouteClass;
348
    }
349
350
    /**
351
     * Load the object associated with the matching object route.
352
     *
353
     * Validating if the object ID exists is delegated to the
354
     * {@see GenericRoute Chainable::pathResolvable()} method.
355
     *
356
     * @return RoutableInterface
357
     */
358
    protected function loadContextObject()
359
    {
360
        if ($this->contextObject) {
361
            return $this->contextObject;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->contextObject; of type Charcoal\Model\ModelInte...bject\RoutableInterface adds the type Charcoal\Model\ModelInterface to the return on line 361 which is incompatible with the return type documented by Charcoal\Cms\Route\GenericRoute::loadContextObject of type Charcoal\Object\RoutableInterface.
Loading history...
362
        }
363
364
        $objectRoute = $this->loadObjectRouteFromPath();
365
366
        $obj = $this->modelFactory()->create($objectRoute->routeObjType());
367
        $obj->load($objectRoute->routeObjId());
368
369
        $this->contextObject = $obj;
370
371
        return $this->contextObject;
372
    }
373
374
    /**
375
     * Load the object route matching the URI path.
376
     *
377
     * @return \Charcoal\Object\ObjectRouteInterface
378
     */
379
    protected function loadObjectRouteFromPath()
380
    {
381
        if ($this->objectRoute) {
382
            return $this->objectRoute;
383
        }
384
385
        // Load current slug
386
        // Slug are uniq
387
        $route = $this->createRouteObject();
388
        $route->loadFromQuery(
389
            'SELECT * FROM `'.$route->source()->table().'` WHERE (`slug` = :route1 OR `slug` = :route2) LIMIT 1',
390
            [
391
                'route1' => '/'.$this->path(),
392
                'route2' => $this->path()
393
            ]
394
        );
395
396
        $this->objectRoute = $route;
397
398
        return $this->objectRoute;
399
    }
400
401
    /**
402
     * Retrieve the latest object route from the given object route's
403
     * associated object.
404
     *
405
     * The object routes are ordered by descending creation date (latest first).
406
     * Should never MISS, the given object route should exist.
407
     *
408
     * @param  ObjectRouteInterface $route Routable Object.
409
     * @return ObjectRouteInterface
410
     */
411
    public function getLatestObjectPathHistory(ObjectRouteInterface $route)
412
    {
413
        $loader = $this->collectionLoader();
414
        $loader
415
            ->setModel($route)
0 ignored issues
show
Documentation introduced by
$route is of type object<Charcoal\Object\ObjectRouteInterface>, but the function expects a string|object<Charcoal\Model\ModelInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
416
            ->addFilter('active', true)
417
            ->addFilter('route_obj_type', $route->routeObjType())
418
            ->addFilter('route_obj_id', $route->routeObjId())
419
            ->addFilter('route_options', '', ['operator' => 'IS NULL'])
420
            ->addFilter('lang', $route->lang())
421
            ->addOrder('creation_date', 'desc')
422
            ->setPage(1)
423
            ->setNumPerPage(1);
424
425
        $collection = $loader->load();
426
        $routes = $collection->objects();
427
428
        $latestRoute = $routes[0];
429
430
        return $latestRoute;
431
    }
432
433
    /**
434
     * SETTERS
435
     */
436
437
    /**
438
     * Set the specified URI path.
439
     *
440
     * @param string $path The path to use for route resolution.
441
     * @return self
442
     */
443
    protected function setPath($path)
444
    {
445
        $this->path = $path;
446
447
        return $this;
448
    }
449
450
    /**
451
     * Set an object model factory.
452
     *
453
     * @param FactoryInterface $factory The model factory, to create objects.
454
     * @return self
455
     */
456
    protected function setModelFactory(FactoryInterface $factory)
457
    {
458
        $this->modelFactory = $factory;
459
460
        return $this;
461
    }
462
463
    /**
464
     * Set a model collection loader.
465
     *
466
     * @param CollectionLoader $loader The collection loader.
467
     * @return self
468
     */
469
    public function setCollectionLoader(CollectionLoader $loader)
470
    {
471
        $this->collectionLoader = $loader;
472
473
        return $this;
474
    }
475
476
    /**
477
     * Sets the environment's current locale.
478
     *
479
     * @param  string $langCode The locale's language code.
480
     * @return void
481
     */
482
    protected function setLocale($langCode)
483
    {
484
        $translator = $this->translator();
485
        $translator->setLocale($langCode);
486
487
        $available = $translator->locales();
488
        $fallbacks = $translator->getFallbackLocales();
489
490
        array_unshift($fallbacks, $langCode);
491
        array_unique($fallbacks);
492
493
        $locales = [];
494
        foreach ($fallbacks as $code) {
495
            if (isset($available[$code])) {
496
                $locale = (array)$available[$code]['locale'];
497
                array_push($locales, ...$locale);
498
            }
499
        }
500
501
        if ($locales) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $locales of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
502
            setlocale(LC_ALL, ...$locales);
503
        }
504
    }
505
506
    /**
507
     * GETTERS
508
     */
509
510
    /**
511
     * Retrieve the URI path.
512
     *
513
     * @return string
514
     */
515
    protected function path()
516
    {
517
        return $this->path;
518
    }
519
520
    /**
521
     * Retrieve the object model factory.
522
     *
523
     * @throws RuntimeException If the model factory was not previously set.
524
     * @return FactoryInterface
525
     */
526 View Code Duplication
    public function modelFactory()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
527
    {
528
        if (!isset($this->modelFactory)) {
529
            throw new RuntimeException(
530
                sprintf('Model Factory is not defined for "%s"', get_class($this))
531
            );
532
        }
533
534
        return $this->modelFactory;
535
    }
536
537
    /**
538
     * Retrieve the model collection loader.
539
     *
540
     * @throws RuntimeException If the collection loader was not previously set.
541
     * @return CollectionLoader
542
     */
543 View Code Duplication
    protected function collectionLoader()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
544
    {
545
        if (!isset($this->collectionLoader)) {
546
            throw new RuntimeException(
547
                sprintf('Collection Loader is not defined for "%s"', get_class($this))
548
            );
549
        }
550
551
        return $this->collectionLoader;
552
    }
553
554
    /**
555
     * @return boolean
556
     */
557
    protected function cacheEnabled()
558
    {
559
        $obj = $this->loadContextObject();
560
        return $obj['cache'] ?: false;
561
    }
562
563
    /**
564
     * @return integer
565
     */
566
    protected function cacheTtl()
567
    {
568
        $obj = $this->loadContextObject();
569
        return $obj['cache_ttl'] ?: 0;
570
    }
571
572
    /**
573
     * @return string
574
     */
575
    protected function cacheIdent()
576
    {
577
        $obj = $this->loadContextObject();
578
        return $obj->objType().'.'.$obj->id();
579
    }
580
}
581