1: <?php
2:
3: namespace Charcoal\Cms;
4:
5: use InvalidArgumentException;
6:
7: // From 'charcoal-core'
8: use Charcoal\Model\Collection;
9: use Charcoal\Loader\CollectionLoader;
10:
11: // From 'charcoal-object'
12: use Charcoal\Object\Content;
13: use Charcoal\Object\HierarchicalInterface;
14: use Charcoal\Object\HierarchicalTrait;
15: use Charcoal\Object\RoutableInterface;
16: use Charcoal\Object\RoutableTrait;
17:
18: // From 'charcoal-translator'
19: use Charcoal\Translator\Translation;
20:
21: // From 'charcoal-cms'
22: use Charcoal\Cms\MetatagInterface;
23: use Charcoal\Cms\SearchableInterface;
24: use Charcoal\Cms\SectionInterface;
25: use Charcoal\Cms\TemplateableInterface;
26:
27: /**
28: * A Section is a unique, reachable page.
29: *
30: * ## Types of sections
31: * There can be different types of section. 4 exists in the CMS module:
32: * - `blocks`
33: * - `content`
34: * - `empty`
35: * - `external`
36: *
37: * ## External implementations
38: * Sections implement the following _Interface_ / _Trait_:
39: * - From the `Charcoal\Object` namespace (in `charcoal-base`)
40: * - `Hierarchical`
41: * - `Routable`
42: * - From the local `Charcoal\Cms` namespace
43: * - `Metatag`
44: * - `Searchable`
45: *
46: */
47: abstract class AbstractSection extends Content implements
48: HierarchicalInterface,
49: MetatagInterface,
50: RoutableInterface,
51: SearchableInterface,
52: SectionInterface,
53: TemplateableInterface
54: {
55: use HierarchicalTrait;
56: use MetatagTrait;
57: use RoutableTrait;
58: use SearchableTrait;
59: use TemplateableTrait;
60:
61: const TYPE_BLOCKS = 'charcoal/cms/section/blocks-section';
62: const TYPE_CONTENT = 'charcoal/cms/section/content-section';
63: const TYPE_EMPTY = 'charcoal/cms/section/empty-section';
64: const TYPE_EXTERNAL = 'charcoal/cms/section/external-section';
65: const DEFAULT_TYPE = self::TYPE_CONTENT;
66:
67: /**
68: * @var string
69: */
70: private $sectionType = self::DEFAULT_TYPE;
71:
72: /**
73: * @var Translation|string|null
74: */
75: private $title;
76:
77: /**
78: * @var Translation|string|null
79: */
80: private $subtitle;
81:
82: /**
83: * @var Translation|string|null
84: */
85: private $content;
86:
87: /**
88: * @var Translation|string|null
89: */
90: private $image;
91:
92: /**
93: * The menus this object is shown in.
94: *
95: * @var string[]
96: */
97: protected $inMenu;
98:
99: /**
100: * @var array
101: */
102: protected $keywords;
103:
104: /**
105: * @var Translation|string $summary
106: */
107: protected $summary;
108:
109: /**
110: * @var string $externalUrl
111: */
112: protected $externalUrl;
113:
114: /**
115: * @var boolean $locked
116: */
117: protected $locked;
118:
119: // ==========================================================================
120: // INIT
121: // ==========================================================================
122:
123: /**
124: * Section constructor.
125: * @param array $data Init data.
126: */
127: public function __construct(array $data = null)
128: {
129: parent::__construct($data);
130:
131: if (is_callable([ $this, 'defaultData' ])) {
132: $this->setData($this->defaultData());
133: }
134: }
135:
136: // ==========================================================================
137: // FUNCTIONS
138: // ==========================================================================
139:
140: /**
141: * Determine if the object can be deleted.
142: *
143: * @return boolean
144: */
145: public function isDeletable()
146: {
147: return !!$this->id() && !$this->locked();
148: }
149:
150: /**
151: * Retrieve the object's title.
152: *
153: * @return string
154: */
155: public function hierarchicalLabel()
156: {
157: return str_repeat('— ', ($this->hierarchyLevel() - 1)).$this->title();
158: }
159:
160: /**
161: * HierarchicalTrait > loadChildren
162: *
163: * @return \ArrayAccess|\Traversable
164: */
165: public function loadChildren()
166: {
167: $loader = new CollectionLoader([
168: 'logger' => $this->logger,
169: 'factory' => $this->modelFactory()
170: ]);
171: $loader->setModel($this);
172: $loader->addFilter([
173: 'property' => 'master',
174: 'val' => $this->id()
175: ]);
176: $loader->addFilter([
177: 'property' => 'active',
178: 'val' => true
179: ]);
180:
181: $loader->addOrder([
182: 'property' => 'position',
183: 'mode' => 'asc'
184: ]);
185:
186: return $loader->load();
187: }
188:
189: // ==========================================================================
190: // SETTERS
191: // ==========================================================================
192:
193: /**
194: * Set the section's type.
195: *
196: * @param string $type The section type.
197: * @throws InvalidArgumentException If the section type is not a string or not a valid section type.
198: * @return self
199: */
200: public function setSectionType($type)
201: {
202: if (!is_string($type)) {
203: throw new InvalidArgumentException(
204: 'Section type must be a string'
205: );
206: }
207:
208: $this->sectionType = $type;
209:
210: return $this;
211: }
212:
213: /**
214: * Set the menus this object belongs to.
215: *
216: * @param string|string[] $menu One or more menu identifiers.
217: * @return self
218: */
219: public function setInMenu($menu)
220: {
221: $this->inMenu = $menu;
222:
223: return $this;
224: }
225:
226: /**
227: * Set the object's keywords.
228: *
229: * @param string|string[] $keywords One or more entries.
230: * @return self
231: */
232: public function setKeywords($keywords)
233: {
234: $this->keywords = $this->parseAsMultiple($keywords);
235:
236: return $this;
237: }
238:
239: /**
240: * @param Translation|string|null $summary The summary.
241: * @return self
242: */
243: public function setSummary($summary)
244: {
245: $this->summary = $this->translator()->translation($summary);
246:
247: return $this;
248: }
249:
250: /**
251: * @param Translation|string|null $externalUrl The external url.
252: * @return self
253: */
254: public function setExternalUrl($externalUrl)
255: {
256: $this->externalUrl = $this->translator()->translation($externalUrl);
257:
258: return $this;
259: }
260:
261: /**
262: * Section is locked when you can't change the URL
263: * @param boolean $locked Prevent new route creation about that object.
264: * @return self
265: */
266: public function setLocked($locked)
267: {
268: $this->locked = $locked;
269:
270: return $this;
271: }
272:
273: /**
274: * @param Translation|string|null $title The section title (localized).
275: * @return self
276: */
277: public function setTitle($title)
278: {
279: $this->title = $this->translator()->translation($title);
280:
281: return $this;
282: }
283:
284: /**
285: * @param Translation|string|null $subtitle The section subtitle (localized).
286: * @return self
287: */
288: public function setSubtitle($subtitle)
289: {
290: $this->subtitle = $this->translator()->translation($subtitle);
291:
292: return $this;
293: }
294:
295: /**
296: * @param Translation|string|null $content The section content (localized).
297: * @return self
298: */
299: public function setContent($content)
300: {
301: $this->content = $this->translator()->translation($content);
302:
303: return $this;
304: }
305:
306: /**
307: * @param mixed $image The section main image (localized).
308: * @return self
309: */
310: public function setImage($image)
311: {
312: $this->image = $this->translator()->translation($image);
313:
314: return $this;
315: }
316:
317: // ==========================================================================
318: // GETTERS
319: // ==========================================================================
320:
321: /**
322: * Retrieve the section's type.
323: *
324: * @return string
325: */
326: public function sectionType()
327: {
328: return $this->sectionType;
329: }
330:
331: /**
332: * @return Translation|string|null
333: */
334: public function title()
335: {
336: return $this->title;
337: }
338:
339: /**
340: * @return Translation|string|null
341: */
342: public function subtitle()
343: {
344: return $this->subtitle;
345: }
346:
347: /**
348: * @return Translation|string|null
349: */
350: public function content()
351: {
352: return $this->content;
353: }
354:
355: /**
356: * @return Translation|string|null
357: */
358: public function image()
359: {
360: return $this->image;
361: }
362:
363: /**
364: * Retrieve the menus this object belongs to.
365: *
366: * @return Translation|string|null
367: */
368: public function inMenu()
369: {
370: return $this->inMenu;
371: }
372:
373: /**
374: * Retrieve the object's keywords.
375: *
376: * @return string[]
377: */
378: public function keywords()
379: {
380: return $this->keywords;
381: }
382:
383: /**
384: * HierarchicalTrait > loadChildren
385: *
386: * @return Translation|string|null
387: */
388: public function summary()
389: {
390: return $this->summary;
391: }
392:
393: /**
394: * @return Translation|string|null
395: */
396: public function externalUrl()
397: {
398: return $this->externalUrl;
399: }
400:
401: /**
402: * @return boolean Or Null.
403: */
404: public function locked()
405: {
406: return $this->locked;
407: }
408:
409: // ==========================================================================
410: // DEFAULT META
411: // ==========================================================================
412:
413: /**
414: * MetatagTrait > canonicalUrl
415: *
416: * @todo
417: * @return string
418: */
419: public function canonicalUrl()
420: {
421: return $this->url();
422: }
423:
424: /**
425: * @return Translation|string|null
426: */
427: public function defaultMetaTitle()
428: {
429: return $this->title();
430: }
431:
432: /**
433: * @return Translation|string|null
434: */
435: public function defaultMetaDescription()
436: {
437: $content = $this->translator()->translation($this->content());
438: if ($content instanceof Translation) {
439: $desc = [];
440: foreach ($content->data() as $lang => $text) {
441: $desc[$lang] = strip_tags($text);
442: }
443:
444: return $this->translator()->translation($desc);
445: }
446:
447: return null;
448: }
449:
450: /**
451: * @return Translation|string|null
452: */
453: public function defaultMetaImage()
454: {
455: return $this->image();
456: }
457:
458: // ==========================================================================
459: // Utils
460: // ==========================================================================
461:
462: /**
463: * Parse the property value as a "multiple" value type.
464: *
465: * @param mixed $value The value being converted to an array.
466: * @param string|PropertyInterface $separator The boundary string.
467: * @return array
468: */
469: public function parseAsMultiple($value, $separator = ',')
470: {
471: if (!isset($value) ||
472: (is_string($value) && !strlen(trim($value))) ||
473: (is_array($value) && !count(array_filter($value, 'strlen')))
474: ) {
475: return [];
476: }
477:
478: /**
479: * This property is marked as "multiple".
480: * Manually handling the resolution to array
481: * until the property itself manages this.
482: */
483: if (is_string($value)) {
484: return explode($separator, $value);
485: }
486:
487: /**
488: * If the parameter isn't an array yet,
489: * means we might be dealing with an integer,
490: * an empty string, or an object.
491: */
492: if (!is_array($value)) {
493: return [ $value ];
494: }
495:
496: return $value;
497: }
498:
499: // ==========================================================================
500: // EVENTS
501: // ==========================================================================
502:
503: /**
504: * Route generated on postSave in case
505: * it contains the ID of the section, which
506: * you only get once you have save
507: *
508: * @return boolean
509: */
510: public function postSave()
511: {
512: // RoutableTrait
513: if (!$this->locked()) {
514: $this->generateObjectRoute($this->slug());
515: }
516:
517: return parent::postSave();
518: }
519:
520: /**
521: * Check whatever before the update.
522: *
523: * @param array|null $properties Properties.
524: * @return boolean
525: */
526: public function postUpdate(array $properties = null)
527: {
528: if (!$this->locked()) {
529: $this->generateObjectRoute($this->slug());
530: }
531:
532: return parent::postUpdate($properties);
533: }
534:
535: /**
536: * {@inheritdoc}
537: *
538: * @return boolean
539: */
540: public function preSave()
541: {
542: if (!$this->locked()) {
543: $this->setSlug($this->generateSlug());
544: }
545:
546: return parent::preSave();
547: }
548:
549: /**
550: * {@inheritdoc}
551: *
552: * @param array $properties Optional properties to update.
553: * @return boolean
554: */
555: public function preUpdate(array $properties = null)
556: {
557: if (!$this->locked()) {
558: $this->setSlug($this->generateSlug());
559: }
560:
561: return parent::preUpdate($properties);
562: }
563:
564: /**
565: * Event called before _deleting_ the object.
566: *
567: * @see \Charcoal\Model\AbstractModel::preDelete() For the "delete" Event.
568: * @return boolean
569: */
570: public function preDelete()
571: {
572: if ($this->locked()) {
573: return false;
574: }
575: // Routable trait
576: // Remove all unnecessary routes.
577: $this->deleteObjectRoutes();
578:
579: return parent::preDelete();
580: }
581: }
582: