Overview

Namespaces

  • Charcoal
    • Admin
      • Widget
        • Cms
    • Cms
      • Config
      • Mixin
        • Traits
      • Route
      • Section
      • Service
        • Loader
        • Manager
      • ServiceProvider
      • Support
        • Helpers
        • Interfaces
        • Traits
    • Property

Classes

  • Charcoal\Admin\Widget\Cms\HierarchicalSectionTableWidget
  • Charcoal\Admin\Widget\Cms\SectionTableWidget
  • Charcoal\Cms\AbstractDocument
  • Charcoal\Cms\AbstractEvent
  • Charcoal\Cms\AbstractFaq
  • Charcoal\Cms\AbstractImage
  • Charcoal\Cms\AbstractNews
  • Charcoal\Cms\AbstractSection
  • Charcoal\Cms\AbstractText
  • Charcoal\Cms\AbstractVideo
  • Charcoal\Cms\Config
  • Charcoal\Cms\Config\CmsConfig
  • Charcoal\Cms\Config\EventConfig
  • Charcoal\Cms\Config\NewsConfig
  • Charcoal\Cms\Config\SectionConfig
  • Charcoal\Cms\Document
  • Charcoal\Cms\DocumentCategory
  • Charcoal\Cms\EmptySection
  • Charcoal\Cms\Event
  • Charcoal\Cms\EventCategory
  • Charcoal\Cms\ExternalSection
  • Charcoal\Cms\Faq
  • Charcoal\Cms\FaqCategory
  • Charcoal\Cms\Image
  • Charcoal\Cms\ImageCategory
  • Charcoal\Cms\News
  • Charcoal\Cms\NewsCategory
  • Charcoal\Cms\Route\EventRoute
  • Charcoal\Cms\Route\GenericRoute
  • Charcoal\Cms\Route\NewsRoute
  • Charcoal\Cms\Route\SectionRoute
  • Charcoal\Cms\Section
  • Charcoal\Cms\Section\BlocksSection
  • Charcoal\Cms\Section\ContentSection
  • Charcoal\Cms\Service\Loader\AbstractLoader
  • Charcoal\Cms\Service\Loader\EventLoader
  • Charcoal\Cms\Service\Loader\NewsLoader
  • Charcoal\Cms\Service\Loader\SectionLoader
  • Charcoal\Cms\Service\Manager\AbstractManager
  • Charcoal\Cms\Service\Manager\EventManager
  • Charcoal\Cms\Service\Manager\NewsManager
  • Charcoal\Cms\ServiceProvider\CmsServiceProvider
  • Charcoal\Cms\Support\Helpers\DateHelper
  • Charcoal\Cms\Tag
  • Charcoal\Cms\Text
  • Charcoal\Cms\TextCategory
  • Charcoal\Cms\Video
  • Charcoal\Cms\VideoCategory
  • Charcoal\Property\TemplateOptionsProperty
  • Charcoal\Property\TemplateProperty

Interfaces

  • Charcoal\Cms\DocumentInterface
  • Charcoal\Cms\EventInterface
  • Charcoal\Cms\FaqInterface
  • Charcoal\Cms\ImageInterface
  • Charcoal\Cms\MetatagInterface
  • Charcoal\Cms\Mixin\HasContentBlocksInterface
  • Charcoal\Cms\NewsInterface
  • Charcoal\Cms\SearchableInterface
  • Charcoal\Cms\SectionInterface
  • Charcoal\Cms\Support\Interfaces\EventManagerAwareInterface
  • Charcoal\Cms\Support\Interfaces\NewsManagerAwareInterface
  • Charcoal\Cms\Support\Interfaces\SectionLoaderAwareInterface
  • Charcoal\Cms\TemplateableInterface
  • Charcoal\Cms\TextInterface
  • Charcoal\Cms\VideoInterface

Traits

  • Charcoal\Admin\Widget\Cms\SectionTableTrait
  • Charcoal\Cms\MetatagTrait
  • Charcoal\Cms\Mixin\Traits\HasContentBlocksTrait
  • Charcoal\Cms\SearchableTrait
  • Charcoal\Cms\Support\Traits\DateHelperAwareTrait
  • Charcoal\Cms\Support\Traits\EventManagerAwareTrait
  • Charcoal\Cms\Support\Traits\NewsManagerAwareTrait
  • Charcoal\Cms\Support\Traits\SectionLoaderAwareTrait
  • Charcoal\Cms\TemplateableTrait
  • Overview
  • Namespace
  • Class
  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:         $currentLang   = $objectRoute->lang();
211: 
212:         // Set language according to the route's language
213:         $this->setLocale($currentLang);
214: 
215:         $templateChoice = [];
216: 
217:         // Templateable Objects have specific methods
218:         if ($contextObject instanceof TemplateableInterface) {
219:             $identProperty = $contextObject->property('template_ident');
220:             $controllerProperty = $contextObject->property('controller_ident');
221: 
222:             // Methods from TemplateableInterface / Trait
223:             $templateIdent = $contextObject->templateIdent() ?: $objectRoute->routeTemplate();
224:             // Default fallback to routeTemplate
225:             $controllerIdent = $contextObject->controllerIdent() ?: $templateIdent;
226: 
227:             $templateChoice = $identProperty->choice($templateIdent);
228:         } else {
229:             // Use global templates to verify for custom paths
230:             $templateIdent = $objectRoute->routeTemplate();
231:             $controllerIdent = $templateIdent;
232:             foreach ($this->availableTemplates as $templateKey => $templateData) {
233:                 if (!isset($templateData['value'])) {
234:                     $templateData['value'] = $templateKey;
235:                 }
236:                 if ($templateData['value'] === $templateIdent) {
237:                     $templateChoice = $templateData;
238:                     break;
239:                 }
240:             }
241:         }
242: 
243:         // Template ident defined in template global config
244:         // Check for custom path / controller
245:         if (isset($templateChoice['template'])) {
246:             $templatePath = $templateChoice['template'];
247:             $templateController = $templateChoice['template'];
248:         } else {
249:             $templatePath = $templateIdent;
250:             $templateController = $controllerIdent;
251:         }
252: 
253:         // Template controller defined in choices, affect it.
254:         if (isset($templateChoice['controller'])) {
255:             $templateController = $templateChoice['controller'];
256:         }
257: 
258:         $config['template'] = $templatePath;
259:         $config['controller'] = $templateController;
260: 
261:         // Always be an array
262:         $templateOptions = [];
263: 
264:         // Custom template options
265:         if (isset($templateChoice['template_options'])) {
266:             $templateOptions = $templateChoice['template_options'];
267:         }
268: 
269:         // Overwrite from custom object template_options
270:         if ($contextObject instanceof TemplateableInterface) {
271:             if (!empty($contextObject->templateOptions())) {
272:                 $templateOptions = $contextObject->templateOptions();
273:             }
274:         }
275: 
276:         if (isset($templateOptions) && $templateOptions) {
277:             // Not sure what this was about?
278:             $config['template_data'] = array_merge($config['template_data'], $templateOptions);
279:         }
280: 
281:         // Merge Route options from object-route
282:         $routeOptions = $objectRoute->routeOptions();
283:         if ($routeOptions && count($routeOptions)) {
284:             $config['template_data'] = array_merge($config['template_data'], $routeOptions);
285:         }
286: 
287:         $this->setConfig($config);
288: 
289:         return $this;
290:     }
291: 
292:     /**
293:      * @param  Container        $container A DI (Pimple) container.
294:      * @param  RequestInterface $request   The request to intialize the template with.
295:      * @return string
296:      */
297:     protected function createTemplate(Container $container, RequestInterface $request)
298:     {
299:         $template = parent::createTemplate($container, $request);
300: 
301:         $contextObject = $this->loadContextObject();
302:         $template->setContextObject($contextObject);
303: 
304:         return $template;
305:     }
306: 
307:     /**
308:      * Create a route object.
309:      *
310:      * @return ObjectRouteInterface
311:      */
312:     public function createRouteObject()
313:     {
314:         $route = $this->modelFactory()->create($this->objectRouteClass());
315: 
316:         return $route;
317:     }
318: 
319:     /**
320:      * Set the class name of the object route model.
321:      *
322:      * @param  string $className The class name of the object route model.
323:      * @throws InvalidArgumentException If the class name is not a string.
324:      * @return self
325:      */
326:     protected function setObjectRouteClass($className)
327:     {
328:         if (!is_string($className)) {
329:             throw new InvalidArgumentException(
330:                 'Route class name must be a string.'
331:             );
332:         }
333: 
334:         $this->objectRouteClass = $className;
335: 
336:         return $this;
337:     }
338: 
339:     /**
340:      * Retrieve the class name of the object route model.
341:      *
342:      * @return string
343:      */
344:     public function objectRouteClass()
345:     {
346:         return $this->objectRouteClass;
347:     }
348: 
349:     /**
350:      * Load the object associated with the matching object route.
351:      *
352:      * Validating if the object ID exists is delegated to the
353:      * {@see GenericRoute Chainable::pathResolvable()} method.
354:      *
355:      * @return RoutableInterface
356:      */
357:     protected function loadContextObject()
358:     {
359:         if ($this->contextObject) {
360:             return $this->contextObject;
361:         }
362: 
363:         $objectRoute = $this->loadObjectRouteFromPath();
364: 
365:         $obj = $this->modelFactory()->create($objectRoute->routeObjType());
366:         $obj->load($objectRoute->routeObjId());
367: 
368:         $this->contextObject = $obj;
369: 
370:         return $this->contextObject;
371:     }
372: 
373:     /**
374:      * Load the object route matching the URI path.
375:      *
376:      * @return \Charcoal\Object\ObjectRouteInterface
377:      */
378:     protected function loadObjectRouteFromPath()
379:     {
380:         if ($this->objectRoute) {
381:             return $this->objectRoute;
382:         }
383: 
384:         // Load current slug
385:         // Slug are uniq
386:         $route = $this->createRouteObject();
387:         $route->loadFromQuery(
388:             'SELECT * FROM `'.$route->source()->table().'` WHERE (`slug` = :route1 OR `slug` = :route2) LIMIT 1',
389:             [
390:                 'route1' => '/'.$this->path(),
391:                 'route2' => $this->path()
392:             ]
393:         );
394: 
395:         $this->objectRoute = $route;
396: 
397:         return $this->objectRoute;
398:     }
399: 
400:     /**
401:      * Retrieve the latest object route from the given object route's
402:      * associated object.
403:      *
404:      * The object routes are ordered by descending creation date (latest first).
405:      * Should never MISS, the given object route should exist.
406:      *
407:      * @param  ObjectRouteInterface $route Routable Object.
408:      * @return ObjectRouteInterface
409:      */
410:     public function getLatestObjectPathHistory(ObjectRouteInterface $route)
411:     {
412:         $loader = $this->collectionLoader();
413:         $loader
414:             ->setModel($route)
415:             ->addFilter('active', true)
416:             ->addFilter('route_obj_type', $route->routeObjType())
417:             ->addFilter('route_obj_id', $route->routeObjId())
418:             ->addFilter('lang', $route->lang())
419:             ->addOrder('creation_date', 'desc')
420:             ->setPage(1)
421:             ->setNumPerPage(1);
422: 
423:         if ($route->routeOptionsIdent()) {
424:             $loader->addFilter('route_options_ident', $route->routeOptionsIdent());
425:         } else {
426:             $loader->addFilter('route_options_ident', '', ['operator' => 'IS NULL']);
427:         }
428: 
429:         $collection = $loader->load();
430:         $routes = $collection->objects();
431: 
432:         $latestRoute = $routes[0];
433: 
434:         return $latestRoute;
435:     }
436: 
437:     /**
438:      * SETTERS
439:      */
440: 
441:     /**
442:      * Set the specified URI path.
443:      *
444:      * @param string $path The path to use for route resolution.
445:      * @return self
446:      */
447:     protected function setPath($path)
448:     {
449:         $this->path = $path;
450: 
451:         return $this;
452:     }
453: 
454:     /**
455:      * Set an object model factory.
456:      *
457:      * @param FactoryInterface $factory The model factory, to create objects.
458:      * @return self
459:      */
460:     protected function setModelFactory(FactoryInterface $factory)
461:     {
462:         $this->modelFactory = $factory;
463: 
464:         return $this;
465:     }
466: 
467:     /**
468:      * Set a model collection loader.
469:      *
470:      * @param CollectionLoader $loader The collection loader.
471:      * @return self
472:      */
473:     public function setCollectionLoader(CollectionLoader $loader)
474:     {
475:         $this->collectionLoader = $loader;
476: 
477:         return $this;
478:     }
479: 
480:     /**
481:      * Sets the environment's current locale.
482:      *
483:      * @param  string $langCode The locale's language code.
484:      * @return void
485:      */
486:     protected function setLocale($langCode)
487:     {
488:         $translator = $this->translator();
489:         $translator->setLocale($langCode);
490: 
491:         $available = $translator->locales();
492:         $fallbacks = $translator->getFallbackLocales();
493: 
494:         array_unshift($fallbacks, $langCode);
495:         $fallbacks = array_unique($fallbacks);
496: 
497:         $locales = [];
498:         foreach ($fallbacks as $code) {
499:             if (isset($available[$code])) {
500:                 $locale = $available[$code];
501:                 if (isset($locale['locales'])) {
502:                     $choices = (array)$locale['locales'];
503:                     array_push($locales, ...$choices);
504:                 } elseif (isset($locale['locale'])) {
505:                     array_push($locales, $locale['locale']);
506:                 }
507:             }
508:         }
509: 
510:         $locales = array_unique($locales);
511: 
512:         if ($locales) {
513:             setlocale(LC_ALL, $locales);
514:         }
515:     }
516: 
517:     /**
518:      * GETTERS
519:      */
520: 
521:     /**
522:      * Retrieve the URI path.
523:      *
524:      * @return string
525:      */
526:     protected function path()
527:     {
528:         return $this->path;
529:     }
530: 
531:     /**
532:      * Retrieve the object model factory.
533:      *
534:      * @throws RuntimeException If the model factory was not previously set.
535:      * @return FactoryInterface
536:      */
537:     public function modelFactory()
538:     {
539:         if (!isset($this->modelFactory)) {
540:             throw new RuntimeException(
541:                 sprintf('Model Factory is not defined for "%s"', get_class($this))
542:             );
543:         }
544: 
545:         return $this->modelFactory;
546:     }
547: 
548:     /**
549:      * Retrieve the model collection loader.
550:      *
551:      * @throws RuntimeException If the collection loader was not previously set.
552:      * @return CollectionLoader
553:      */
554:     protected function collectionLoader()
555:     {
556:         if (!isset($this->collectionLoader)) {
557:             throw new RuntimeException(
558:                 sprintf('Collection Loader is not defined for "%s"', get_class($this))
559:             );
560:         }
561: 
562:         return $this->collectionLoader;
563:     }
564: 
565:     /**
566:      * @return boolean
567:      */
568:     protected function cacheEnabled()
569:     {
570:         $obj = $this->loadContextObject();
571:         return $obj['cache'] ?: false;
572:     }
573: 
574:     /**
575:      * @return integer
576:      */
577:     protected function cacheTtl()
578:     {
579:         $obj = $this->loadContextObject();
580:         return $obj['cache_ttl'] ?: 0;
581:     }
582: 
583:     /**
584:      * @return string
585:      */
586:     protected function cacheIdent()
587:     {
588:         $obj = $this->loadContextObject();
589:         return $obj->objType().'.'.$obj->id();
590:     }
591: }
592: 
API documentation generated by ApiGen