Completed
Push — master ( 98260d...5a7546 )
by Mathieu
03:43
created

GenericRoute::cacheIdent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 5
rs 9.4285
cc 1
eloc 3
nc 1
nop 0
1
<?php
2
3
namespace Charcoal\Cms\Route;
4
5
use InvalidArgumentException;
6
use RuntimeException;
7
8
use Pimple\Container;
9
10
// From PSR-7 (HTTP Messaging)
11
use Psr\Http\Message\RequestInterface;
12
use Psr\Http\Message\ResponseInterface;
13
14
// Dependency from 'charcoal-app'
15
use Charcoal\App\Route\TemplateRoute;
16
17
// Dependency from 'charcoal-cms'
18
use Charcoal\Cms\TemplateableInterface;
19
20
// From 'charcoal-factory'
21
use Charcoal\Factory\FactoryInterface;
22
23
// From 'charcoal-core'
24
use Charcoal\Model\ModelInterface;
25
use Charcoal\Loader\CollectionLoader;
26
27
// From 'charcoal-translation'
28
use Charcoal\Translation\TranslationConfig;
29
30
// From 'charcoal-object'
31
use Charcoal\Object\ObjectRoute;
32
use Charcoal\Object\ObjectRouteInterface;
33
use Charcoal\Object\RoutableInterface;
34
35
/**
36
 * Generic Object Route Handler
37
 *
38
 * Uses implementations of {@see \Charcoal\Object\ObjectRouteInterface}
39
 * to match routes for catch-all routing patterns.
40
 */
41
class GenericRoute extends TemplateRoute
42
{
43
    /**
44
     * The URI path.
45
     *
46
     * @var string
47
     */
48
    private $path;
49
50
    /**
51
     * The object route.
52
     *
53
     * @var ObjectRouteInterface
54
     */
55
    private $objectRoute;
56
57
    /**
58
     * The target object of the {@see GenericRoute Chainable::$objectRoute}.
59
     *
60
     * @var ModelInterface|RoutableInterface
61
     */
62
    private $contextObject;
63
64
    /**
65
     * Store the factory instance for the current class.
66
     *
67
     * @var FactoryInterface
68
     */
69
    private $modelFactory;
70
71
    /**
72
     * Store the collection loader for the current class.
73
     *
74
     * @var CollectionLoader
75
     */
76
    private $collectionLoader;
77
78
    /**
79
     * The class name of the object route model.
80
     *
81
     * Must be a fully-qualified PHP namespace and an implementation of
82
     * {@see \Charcoal\Object\ObjectRouteInterface}. Used by the model factory.
83
     *
84
     * @var string
85
     */
86
    protected $objectRouteClass = ObjectRoute::class;
87
88
    /**
89
     * @var array $availableTemplates
90
     */
91
    protected $availableTemplates = [];
92
93
    /**
94
     * Setting Language on TranslatorConfig allows to seT local properly.
95
     *
96
     * @var TranslatorConfig $translatorConfig
97
     */
98
    protected $translatorConfig;
99
100
    /**
101
     * Returns new template route object.
102
     *
103
     * @param array|\ArrayAccess $data Class depdendencies.
104
     */
105
    public function __construct($data)
106
    {
107
        parent::__construct($data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 105 can also be of type object<ArrayAccess>; however, Charcoal\App\Route\TemplateRoute::__construct() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
108
109
        $this->setPath(ltrim($data['path'], '/'));
110
    }
111
112
    /**
113
     * Inject dependencies from a DI Container.
114
     *
115
     * @param  Container $container A dependencies container instance.
116
     * @return void
117
     */
118
    public function setDependencies(Container $container)
119
    {
120
        $this->translatorConfig = $container['translator/config'];
121
        $this->setModelFactory($container['model/factory']);
122
        $this->setCollectionLoader($container['model/collection/loader']);
123
        if (isset($container['config']['templates'])) {
124
            $this->availableTemplates = $container['config']['templates'];
125
        }
126
    }
127
128
    /**
129
     * Determine if the URI path resolves to an object.
130
     *
131
     * @param  Container $container A DI (Pimple) container.
132
     * @return boolean
133
     */
134
    public function pathResolvable(Container $container)
135
    {
136
        $this->setDependencies($container);
137
138
        $object = $this->loadObjectRouteFromPath();
139
        if (!$object->id()) {
140
            return false;
141
        }
142
143
        $contextObject = $this->loadContextObject();
144
145
        if (!$contextObject || !$contextObject->id()) {
146
            return false;
147
        }
148
149
        return !!$contextObject->active();
150
    }
151
152
    /**
153
     * Resolve the dynamic route.
154
     *
155
     * @param  Container         $container A DI (Pimple) container.
156
     * @param  RequestInterface  $request   A PSR-7 compatible Request instance.
157
     * @param  ResponseInterface $response  A PSR-7 compatible Response instance.
158
     * @return ResponseInterface
159
     */
160
    public function __invoke(
161
        Container $container,
162
        RequestInterface $request,
163
        ResponseInterface $response
164
    ) {
165
        $response = $this->resolveLatestObjectRoute($request, $response);
166
167
        if (!$response->isRedirect()) {
168
            $this->resolveTemplateContextObject();
169
170
            $templateContent = $this->templateContent($container, $request);
171
172
            $response->write($templateContent);
173
        }
174
175
        return $response;
176
    }
177
178
    /**
179
     * @param  RequestInterface  $request  A PSR-7 compatible Request instance.
180
     * @param  ResponseInterface $response A PSR-7 compatible Response instance.
181
     * @return ResponseInterface
182
     */
183
    protected function resolveLatestObjectRoute(
184
        RequestInterface $request,
185
        ResponseInterface $response
186
    ) {
187
        // Current object route
188
        $objectRoute = $this->loadObjectRouteFromPath();
189
190
        // Could be the SAME as current object route
191
        $latest = $this->getLatestObjectPathHistory($objectRoute);
192
193
        // Redirect if latest route is newer
194
        if ($latest->creationDate() > $objectRoute->creationDate()) {
195
            $redirection = $this->parseRedirect($latest->slug(), $request);
196
            $response = $response->withRedirect($redirection, 301);
197
        }
198
199
        return $response;
200
    }
201
202
    /**
203
     * @return GenericRoute Chainable
204
     */
205
    protected function resolveTemplateContextObject()
206
    {
207
        $config = $this->config();
208
209
        $objectRoute = $this->loadObjectRouteFromPath();
210
        $contextObject = $this->loadContextObject();
211
212
        // Set language according to the route's language
213
        $translator = TranslationConfig::instance();
214
        $translator->setCurrentLanguage($objectRoute->lang());
215
216
        $locale = $translator->language($translator->currentLanguage())->locale();
217
        setlocale(LC_ALL, $locale);
218
219
        $templateChoice = [];
220
221
        // Templateable Objects have specific methods
222
        if ($contextObject instanceof TemplateableInterface) {
223
            $identProperty = $contextObject->property('template_ident');
224
            $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...
225
226
            // Methods from TemplateableInterface / Trait
227
            $templateIdent = $contextObject->templateIdent() ?: $objectRoute->routeTemplate();
228
            // Default fallback to routeTemplate
229
            $controllerIdent = $contextObject->controllerIdent() ?: $templateIdent;
230
231
            $templateChoice = $identProperty->choice($templateIdent);
232
        } else {
233
            // Use global templates to verify for custom paths
234
            $templateIdent = $objectRoute->routeTemplate();
235
            $controllerIdent = $templateIdent;
236
            foreach ($this->availableTemplates as $templateKey => $templateData) {
237
                if (!isset($templateData['value'])) {
238
                    $templateData['value'] = $templateKey;
239
                }
240
                if ($templateData['value'] === $templateIdent) {
241
                    $templateChoice = $templateData;
242
                    break;
243
                }
244
            }
245
        }
246
247
        // Template ident defined in template global config
248
        // Check for custom path / controller
249
        if (isset($templateChoice['template'])) {
250
            $templatePath = $templateChoice['template'];
251
            $templateController = $templateChoice['template'];
252
        } else {
253
            $templatePath = $templateIdent;
254
            $templateController = $controllerIdent;
255
        }
256
257
        // Template controller defined in choices, affect it.
258
        if (isset($templateChoice['controller'])) {
259
            $templateController = $templateChoice['controller'];
260
        }
261
262
        $config['template'] = $templatePath;
263
        $config['controller'] = $templateController;
264
265
        // Always be an array
266
        $templateOptions = [];
267
268
        // Custom template options
269
        if (isset($templateChoice['template_options'])) {
270
            $templateOptions = $templateChoice['template_options'];
271
        }
272
273
        // Overwrite from custom object template_options
274
        if ($contextObject instanceof TemplateableInterface) {
275
            if (!empty($contextObject->templateOptions())) {
276
                $templateOptions = $contextObject->templateOptions();
277
            }
278
        }
279
280
        if (isset($templateOptions) && $templateOptions) {
281
            // Not sure what this was about?
282
            $config['template_data'] = array_merge($config['template_data'], $templateOptions);
283
        }
284
285
        $this->setConfig($config);
286
287
        return $this;
288
    }
289
290
    /**
291
     * @param  Container        $container A DI (Pimple) container.
292
     * @param  RequestInterface $request   The request to intialize the template with.
293
     * @return string
294
     */
295
    protected function createTemplate(Container $container, RequestInterface $request)
296
    {
297
        $template = parent::createTemplate($container, $request);
298
299
        $contextObject = $this->loadContextObject();
300
        $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...
301
302
        return $template;
303
    }
304
305
    /**
306
     * Create a route object.
307
     *
308
     * @return ObjectRouteInterface
309
     */
310
    public function createRouteObject()
311
    {
312
        $route = $this->modelFactory()->create($this->objectRouteClass());
313
314
        return $route;
315
    }
316
317
    /**
318
     * Set the class name of the object route model.
319
     *
320
     * @param  string $className The class name of the object route model.
321
     * @throws InvalidArgumentException If the class name is not a string.
322
     * @return GenericRoute Chainable
323
     */
324
    protected function setObjectRouteClass($className)
325
    {
326
        if (!is_string($className)) {
327
            throw new InvalidArgumentException(
328
                'Route class name must be a string.'
329
            );
330
        }
331
332
        $this->objectRouteClass = $className;
333
334
        return $this;
335
    }
336
337
    /**
338
     * Retrieve the class name of the object route model.
339
     *
340
     * @return string
341
     */
342
    public function objectRouteClass()
343
    {
344
        return $this->objectRouteClass;
345
    }
346
347
    /**
348
     * Load the object associated with the matching object route.
349
     *
350
     * Validating if the object ID exists is delegated to the
351
     * {@see GenericRoute Chainable::pathResolvable()} method.
352
     *
353
     * @return RoutableInterface
354
     */
355
    protected function loadContextObject()
356
    {
357
        if ($this->contextObject) {
358
            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 358 which is incompatible with the return type documented by Charcoal\Cms\Route\GenericRoute::loadContextObject of type Charcoal\Object\RoutableInterface.
Loading history...
359
        }
360
361
        $objectRoute = $this->loadObjectRouteFromPath();
362
363
        $obj = $this->modelFactory()->create($objectRoute->routeObjType());
364
        $obj->load($objectRoute->routeObjId());
365
366
        $this->contextObject = $obj;
367
368
        return $this->contextObject;
369
    }
370
371
    /**
372
     * Load the object route matching the URI path.
373
     *
374
     * @return \Charcoal\Object\ObjectRouteInterface
375
     */
376
    protected function loadObjectRouteFromPath()
377
    {
378
        if ($this->objectRoute) {
379
            return $this->objectRoute;
380
        }
381
382
        // Load current slug
383
        // Slug are uniq
384
        $route = $this->createRouteObject();
385
        $route->loadFromQuery(
386
            'SELECT * FROM `'.$route->source()->table().'` WHERE (`slug` = :route1 OR `slug` = :route2) LIMIT 1',
387
            [
388
                'route1' => '/'.$this->path(),
389
                'route2' => $this->path()
390
            ]
391
        );
392
393
        $this->objectRoute = $route;
394
395
        return $this->objectRoute;
396
    }
397
398
    /**
399
     * Retrieve the latest object route from the given object route's
400
     * associated object.
401
     *
402
     * The object routes are ordered by descending creation date (latest first).
403
     * Should never MISS, the given object route should exist.
404
     *
405
     * @param  ObjectRouteInterface $route Routable Object.
406
     * @return ObjectRouteInterface
407
     */
408
    public function getLatestObjectPathHistory(ObjectRouteInterface $route)
409
    {
410
        $loader = $this->collectionLoader();
411
        $loader
412
            ->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...
413
            ->addFilter('active', true)
414
            ->addFilter('route_obj_type', $route->routeObjType())
415
            ->addFilter('route_obj_id', $route->routeObjId())
416
            ->addFilter('lang', $route->lang())
417
            ->addOrder('creation_date', 'desc')
418
            ->setPage(1)
419
            ->setNumPerPage(1);
420
421
        $collection = $loader->load();
422
        $routes = $collection->objects();
423
424
        $latestRoute = $routes[0];
425
426
        return $latestRoute;
427
    }
428
429
    /**
430
     * SETTERS
431
     */
432
433
    /**
434
     * Set the specified URI path.
435
     *
436
     * @param string $path The path to use for route resolution.
437
     * @return GenericRoute Chainable
438
     */
439
    protected function setPath($path)
440
    {
441
        $this->path = $path;
442
443
        return $this;
444
    }
445
446
    /**
447
     * Set an object model factory.
448
     *
449
     * @param FactoryInterface $factory The model factory, to create objects.
450
     * @return GenericRoute Chainable
451
     */
452
    protected function setModelFactory(FactoryInterface $factory)
453
    {
454
        $this->modelFactory = $factory;
455
456
        return $this;
457
    }
458
459
    /**
460
     * Set a model collection loader.
461
     *
462
     * @param CollectionLoader $loader The collection loader.
463
     * @return GenericRoute Chainable
464
     */
465
    public function setCollectionLoader(CollectionLoader $loader)
466
    {
467
        $this->collectionLoader = $loader;
468
469
        return $this;
470
    }
471
472
    /**
473
     * GETTERS
474
     */
475
476
    /**
477
     * Retrieve the URI path.
478
     *
479
     * @return string
480
     */
481
    protected function path()
482
    {
483
        return $this->path;
484
    }
485
486
    /**
487
     * Retrieve the object model factory.
488
     *
489
     * @throws RuntimeException If the model factory was not previously set.
490
     * @return FactoryInterface
491
     */
492
    public function modelFactory()
493
    {
494
        if (!isset($this->modelFactory)) {
495
            throw new RuntimeException(
496
                sprintf('Model Factory is not defined for "%s"', get_class($this))
497
            );
498
        }
499
500
        return $this->modelFactory;
501
    }
502
503
    /**
504
     * Retrieve the model collection loader.
505
     *
506
     * @throws RuntimeException If the collection loader was not previously set.
507
     * @return CollectionLoader
508
     */
509
    protected function collectionLoader()
510
    {
511
        if (!isset($this->collectionLoader)) {
512
            throw new RuntimeException(
513
                sprintf('Collection Loader is not defined for "%s"', get_class($this))
514
            );
515
        }
516
517
        return $this->collectionLoader;
518
    }
519
520
    /**
521
     * @return boolean
522
     */
523
    protected function cacheEnabled()
524
    {
525
        $obj = $this->loadContextObject();
526
        return $obj['cache'] ?: false;
527
    }
528
529
    /**
530
     * @return integer
531
     */
532
    protected function cacheTtl()
533
    {
534
        $obj = $this->loadContextObject();
535
        return $obj['cache_ttl'] ?: 0;
536
    }
537
538
    /**
539
     * @return string
540
     */
541
    protected function cacheIdent()
542
    {
543
        $obj = $this->loadContextObject();
544
        return $obj->objType().'.'.$obj->id();
545
    }
546
}
547