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
|
|
View Code Duplication |
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
|
|
View Code Duplication |
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
|
|
View Code Duplication |
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
|
|
|
|
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.