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); |
|
|
|
|
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'); |
|
|
|
|
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); |
|
|
|
|
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; |
|
|
|
|
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) |
|
|
|
|
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
|
|
|
|
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.