1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Charcoal\Cms; |
4
|
|
|
|
5
|
|
|
use ArrayIterator; |
6
|
|
|
use RuntimeException; |
7
|
|
|
|
8
|
|
|
// From 'psr/http-message' |
9
|
|
|
use Psr\Http\Message\UriInterface; |
10
|
|
|
|
11
|
|
|
// From 'pimple/pimple' |
12
|
|
|
use Pimple\Container; |
13
|
|
|
|
14
|
|
|
// From 'charcoal-core' |
15
|
|
|
use Charcoal\Model\ModelInterface; |
16
|
|
|
|
17
|
|
|
// From 'charcoal-translator' |
18
|
|
|
use Charcoal\Translator\TranslatorAwareTrait; |
19
|
|
|
|
20
|
|
|
// From 'charcoal-app' |
21
|
|
|
use Charcoal\App\AppConfig; |
22
|
|
|
use Charcoal\App\DebugAwareTrait; |
23
|
|
|
use Charcoal\App\Template\AbstractTemplate; |
24
|
|
|
|
25
|
|
|
// From 'charcoal-core' |
26
|
|
|
use Charcoal\Model\ModelFactoryTrait; |
27
|
|
|
|
28
|
|
|
// From 'charcoal-cms' |
29
|
|
|
use Charcoal\Cms\MetatagInterface; |
30
|
|
|
use Charcoal\Cms\Support\ContextualTemplateTrait; |
31
|
|
|
use Charcoal\Cms\Support\DocumentTrait; |
32
|
|
|
use Charcoal\Cms\Support\LocaleAwareTrait; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Hypertext Template Controller |
36
|
|
|
* |
37
|
|
|
* This class acts as an enhancer to the basic abstract template. |
38
|
|
|
*/ |
39
|
|
|
abstract class AbstractWebTemplate extends AbstractTemplate |
40
|
|
|
{ |
41
|
|
|
use ContextualTemplateTrait; |
42
|
|
|
use DebugAwareTrait; |
43
|
|
|
use DocumentTrait; |
44
|
|
|
use LocaleAwareTrait; |
45
|
|
|
use ModelFactoryTrait; |
46
|
|
|
use TranslatorAwareTrait; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* The application's configuration container. |
50
|
|
|
* |
51
|
|
|
* @var AppConfig |
52
|
|
|
*/ |
53
|
|
|
protected $appConfig; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* The base URI. |
57
|
|
|
* |
58
|
|
|
* @var UriInterface|null |
59
|
|
|
*/ |
60
|
|
|
protected $baseUrl; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* The default image for social media sharing. |
64
|
|
|
* |
65
|
|
|
* @var string |
66
|
|
|
*/ |
67
|
|
|
const DEFAULT_SOCIAL_MEDIA_IMAGE = ''; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Additional SEO metadata. |
71
|
|
|
* |
72
|
|
|
* @var array |
73
|
|
|
*/ |
74
|
|
|
private $seoMetadata = []; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Inject dependencies from a DI Container. |
78
|
|
|
* |
79
|
|
|
* @param Container $container A dependencies container instance. |
80
|
|
|
* @return void |
81
|
|
|
*/ |
82
|
|
|
protected function setDependencies(Container $container) |
83
|
|
|
{ |
84
|
|
|
parent::setDependencies($container); |
85
|
|
|
|
86
|
|
|
$this->setAppConfig($container['config']); |
87
|
|
|
$this->setBaseUrl($container['base-url']); |
88
|
|
|
$this->setDebug($container['debug']); |
89
|
|
|
$this->setModelFactory($container['model/factory']); |
90
|
|
|
$this->setTranslator($container['translator']); |
91
|
|
|
|
92
|
|
|
$metatags = $this->appConfig('cms.metatags'); |
93
|
|
|
if (is_array($metatags)) { |
94
|
|
|
$this->setSeoMetadata($metatags); |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Retrieve the title of the page (the context). |
100
|
|
|
* |
101
|
|
|
* @return string|null |
102
|
|
|
*/ |
103
|
|
|
public function title() |
104
|
|
|
{ |
105
|
|
|
$context = $this->contextObject(); |
106
|
|
|
|
107
|
|
|
if ($context && isset($context['title'])) { |
108
|
|
|
return $context['title']; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
return ''; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Retrieve the current URI of the context. |
116
|
|
|
* |
117
|
|
|
* @return \Psr\Http\Message\UriInterface|null |
118
|
|
|
*/ |
119
|
|
View Code Duplication |
public function currentUrl() |
|
|
|
|
120
|
|
|
{ |
121
|
|
|
$context = $this->contextObject(); |
122
|
|
|
|
123
|
|
|
if ($context && isset($context['url'])) { |
124
|
|
|
return $this->createAbsoluteUrl($context['url']); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
return null; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* Retrieve the current locale. |
132
|
|
|
* |
133
|
|
|
* @return string|null |
134
|
|
|
*/ |
135
|
|
|
public function currentLocale() |
136
|
|
|
{ |
137
|
|
|
$langCode = $this->translator()->getLocale(); |
138
|
|
|
$locales = $this->translator()->locales(); |
139
|
|
|
if (isset($locales[$langCode])) { |
140
|
|
|
$locale = $locales[$langCode]; |
141
|
|
|
if (isset($locale['locale'])) { |
142
|
|
|
return $locale['locale']; |
143
|
|
|
} else { |
144
|
|
|
return $langCode; |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
return null; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* Retrieve the current locale's language code. |
153
|
|
|
* |
154
|
|
|
* @return string |
155
|
|
|
*/ |
156
|
|
|
public function currentLanguage() |
157
|
|
|
{ |
158
|
|
|
return $this->translator()->getLocale(); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
|
162
|
|
|
|
163
|
|
|
// Metadata |
164
|
|
|
// ========================================================================= |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Retrieve the canonical URI of the object. |
168
|
|
|
* |
169
|
|
|
* @return \Psr\Http\Message\UriInterface|string|null |
170
|
|
|
*/ |
171
|
|
|
public function canonicalUrl() |
172
|
|
|
{ |
173
|
|
|
return $this->currentUrl(); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Parse the document title parts. |
178
|
|
|
* |
179
|
|
|
* @return string[] |
180
|
|
|
*/ |
181
|
|
|
protected function documentTitleParts() |
182
|
|
|
{ |
183
|
|
|
return [ |
184
|
|
|
'title' => $this->metaTitle(), |
185
|
|
|
'site' => $this->siteName(), |
186
|
|
|
]; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Retrieve the name or title of the object. |
191
|
|
|
* |
192
|
|
|
* @return string|null |
193
|
|
|
*/ |
194
|
|
|
public function metaTitle() |
195
|
|
|
{ |
196
|
|
|
$context = $this->contextObject(); |
197
|
|
|
$title = null; |
198
|
|
|
|
199
|
|
|
if ($context instanceof MetatagInterface) { |
200
|
|
|
$title = (string)$context['metaTitle']; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
if (!$title) { |
|
|
|
|
204
|
|
|
$title = (string)$this->fallbackMetaTitle(); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
return $title; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Hook called as a fallback if no meta title is set on the object. |
212
|
|
|
* |
213
|
|
|
* This method should be extended by child controllers. |
214
|
|
|
* |
215
|
|
|
* @return string|null |
216
|
|
|
*/ |
217
|
|
|
protected function fallbackMetaTitle() |
218
|
|
|
{ |
219
|
|
|
return (string)$this->title(); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* Retrieve the description of the object. |
224
|
|
|
* |
225
|
|
|
* @return string|null |
226
|
|
|
*/ |
227
|
|
View Code Duplication |
public function metaDescription() |
|
|
|
|
228
|
|
|
{ |
229
|
|
|
$context = $this->contextObject(); |
230
|
|
|
|
231
|
|
|
$desc = null; |
232
|
|
|
if ($context instanceof MetatagInterface) { |
233
|
|
|
$desc = (string)$context['metaDescription']; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
if (!$desc) { |
|
|
|
|
237
|
|
|
$desc = (string)$this->fallbackMetaDescription(); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
return $desc; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* Hook called as a fallback if no meta description is set on the object. |
245
|
|
|
* |
246
|
|
|
* This method should be extended by child controllers. |
247
|
|
|
* |
248
|
|
|
* @return string|null |
249
|
|
|
*/ |
250
|
|
|
protected function fallbackMetaDescription() |
251
|
|
|
{ |
252
|
|
|
return null; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* Retrieve the URL to the image representing the object. |
257
|
|
|
* |
258
|
|
|
* @return string|null |
259
|
|
|
*/ |
260
|
|
|
public function metaImage() |
261
|
|
|
{ |
262
|
|
|
$context = $this->contextObject(); |
263
|
|
|
|
264
|
|
|
$img = null; |
265
|
|
|
if ($context instanceof MetatagInterface) { |
266
|
|
|
$img = (string)$context['metaImage']; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
if (!$img) { |
|
|
|
|
270
|
|
|
$img = (string)$this->fallbackMetaImage(); |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
return $this->resolveMetaImage($img); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* Hook called as a fallback if no meta image is set on the object. |
278
|
|
|
* |
279
|
|
|
* This method should be extended by child controllers. |
280
|
|
|
* |
281
|
|
|
* @return string|null |
282
|
|
|
*/ |
283
|
|
|
protected function fallbackMetaImage() |
284
|
|
|
{ |
285
|
|
|
return null; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
/** |
289
|
|
|
* Retrieve the URL to the image representing the object. |
290
|
|
|
* |
291
|
|
|
* @param string|null $img A path to an image. |
292
|
|
|
* @return string|null |
293
|
|
|
*/ |
294
|
|
|
protected function resolveMetaImage($img = null) |
295
|
|
|
{ |
296
|
|
|
if (!$img) { |
|
|
|
|
297
|
|
|
$img = static::DEFAULT_SOCIAL_MEDIA_IMAGE; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
if ($img) { |
301
|
|
|
$uri = $this->baseUrl(); |
302
|
|
|
return $uri->withPath(strval($img)); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
return null; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Retrieve the object's {@link https://developers.facebook.com/docs/reference/opengraph/ OpenGraph type}, |
310
|
|
|
* for the "og:type" meta-property. |
311
|
|
|
* |
312
|
|
|
* @return string|null |
313
|
|
|
*/ |
314
|
|
View Code Duplication |
public function opengraphType() |
|
|
|
|
315
|
|
|
{ |
316
|
|
|
$context = $this->contextObject(); |
317
|
|
|
|
318
|
|
|
$type = null; |
319
|
|
|
|
320
|
|
|
if ($context instanceof MetatagInterface) { |
321
|
|
|
$type = $context['opengraphType']; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
if (!$type) { |
325
|
|
|
$type = MetatagInterface::DEFAULT_OPENGRAPH_TYPE; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
return $type; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
/** |
332
|
|
|
* Retrieve the URL to the object's social image for the "og:image" meta-property. |
333
|
|
|
* |
334
|
|
|
* This method can fallback onto {@see MetadataInterface::defaultMetaImage()} |
335
|
|
|
* for a common image between web annotation schemas. |
336
|
|
|
* |
337
|
|
|
* @return string|null |
338
|
|
|
*/ |
339
|
|
|
public function opengraphImage() |
340
|
|
|
{ |
341
|
|
|
$context = $this->contextObject(); |
342
|
|
|
|
343
|
|
|
$img = null; |
344
|
|
|
if ($context instanceof MetatagInterface) { |
345
|
|
|
$img = (string)$context['opengraphImage']; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
if (!$img) { |
|
|
|
|
349
|
|
|
$img = (string)$this->fallbackOpengraphImage(); |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
if ($img) { |
353
|
|
|
$uri = $this->baseUrl(); |
354
|
|
|
return $uri->withPath(strval($img)); |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
return $this->metaImage(); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* Hook called as a fallback if no social image is set on the object. |
362
|
|
|
* |
363
|
|
|
* This method should be extended by child controllers. |
364
|
|
|
* |
365
|
|
|
* @return string|null |
366
|
|
|
*/ |
367
|
|
|
protected function fallbackOpengraphImage() |
368
|
|
|
{ |
369
|
|
|
return null; |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
/** |
373
|
|
|
* Set additional SEO metadata. |
374
|
|
|
* |
375
|
|
|
* @return iterable |
376
|
|
|
*/ |
377
|
|
|
public function seoMetadata() |
378
|
|
|
{ |
379
|
|
|
return $this->seoMetadata; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
/** |
383
|
|
|
* Determine if we have additional SEO metadata. |
384
|
|
|
* |
385
|
|
|
* @return boolean |
386
|
|
|
*/ |
387
|
|
|
public function hasSeoMetadata() |
388
|
|
|
{ |
389
|
|
|
if ($this->seoMetadata instanceof ArrayIterator) { |
390
|
|
|
return (count($this->seoMetadata) > 0); |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
return !empty($this->seoMetadata); |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
/** |
397
|
|
|
* Set additional SEO metadata. |
398
|
|
|
* |
399
|
|
|
* @param array $metadata Map of metadata keys and values. |
400
|
|
|
* @return self |
401
|
|
|
*/ |
402
|
|
|
protected function setSeoMetadata(array $metadata) |
403
|
|
|
{ |
404
|
|
|
if (is_array($this->seoMetadata)) { |
405
|
|
|
$this->seoMetadata = new ArrayIterator($this->seoMetadata); |
|
|
|
|
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
foreach ($metadata as $key => $value) { |
409
|
|
|
if (is_array($value)) { |
410
|
|
|
$value = implode(',', $value); |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
$this->seoMetadata[] = [ |
414
|
|
|
'name' => $key, |
415
|
|
|
'content' => (string)$value |
416
|
|
|
]; |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
return $this; |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
|
423
|
|
|
|
424
|
|
|
// App |
425
|
|
|
// ========================================================================= |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* Set the application's configset. |
429
|
|
|
* |
430
|
|
|
* @param AppConfig $appConfig A Charcoal application configset. |
431
|
|
|
* @return self |
432
|
|
|
*/ |
433
|
|
|
protected function setAppConfig(AppConfig $appConfig) |
434
|
|
|
{ |
435
|
|
|
$this->appConfig = $appConfig; |
436
|
|
|
|
437
|
|
|
return $this; |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
/** |
441
|
|
|
* Retrieve the application's configset or a specific setting. |
442
|
|
|
* |
443
|
|
|
* @param string|null $key Optional data key to retrieve from the configset. |
444
|
|
|
* @param mixed|null $default The default value to return if data key does not exist. |
445
|
|
|
* @return mixed|AppConfig|SettingsInterface |
446
|
|
|
*/ |
447
|
|
|
public function appConfig($key = null, $default = null) |
448
|
|
|
{ |
449
|
|
|
if ($key) { |
|
|
|
|
450
|
|
|
if (isset($this->appConfig[$key])) { |
451
|
|
|
return $this->appConfig[$key]; |
452
|
|
|
} else { |
453
|
|
|
if (!is_string($default) && is_callable($default)) { |
454
|
|
|
return $default(); |
455
|
|
|
} else { |
456
|
|
|
return $default; |
457
|
|
|
} |
458
|
|
|
} |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
return $this->appConfig; |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
/** |
465
|
|
|
* Set the base URI of the project. |
466
|
|
|
* |
467
|
|
|
* @see \Charcoal\App\ServiceProvider\AppServiceProvider `$container['base-url']` |
468
|
|
|
* @param UriInterface $uri The base URI. |
469
|
|
|
* @return self |
470
|
|
|
*/ |
471
|
|
|
protected function setBaseUrl(UriInterface $uri) |
472
|
|
|
{ |
473
|
|
|
$this->baseUrl = $uri; |
474
|
|
|
|
475
|
|
|
return $this; |
476
|
|
|
} |
477
|
|
|
|
478
|
|
|
/** |
479
|
|
|
* Retrieve the base URI of the project. |
480
|
|
|
* |
481
|
|
|
* @throws RuntimeException If the base URI is missing. |
482
|
|
|
* @return UriInterface|null |
483
|
|
|
*/ |
484
|
|
|
public function baseUrl() |
485
|
|
|
{ |
486
|
|
|
if (!isset($this->baseUrl)) { |
487
|
|
|
throw new RuntimeException(sprintf( |
488
|
|
|
'The base URI is not defined for [%s]', |
489
|
|
|
get_class($this) |
490
|
|
|
)); |
491
|
|
|
} |
492
|
|
|
|
493
|
|
|
return $this->baseUrl; |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
/** |
497
|
|
|
* Prepend the base URI to the given path. |
498
|
|
|
* |
499
|
|
|
* @param string $uri A URI path to wrap. |
500
|
|
|
* @return UriInterface |
501
|
|
|
*/ |
502
|
|
|
public function createAbsoluteUrl($uri) |
503
|
|
|
{ |
504
|
|
|
$uri = strval($uri); |
505
|
|
|
if ($uri === '') { |
506
|
|
|
$uri = $this->baseUrl()->withPath(''); |
507
|
|
|
} else { |
508
|
|
|
$parts = parse_url($uri); |
509
|
|
|
if (!isset($parts['scheme'])) { |
510
|
|
|
if (!in_array($uri[0], [ '/', '#', '?' ])) { |
511
|
|
|
$path = isset($parts['path']) ? $parts['path'] : ''; |
512
|
|
|
$query = isset($parts['query']) ? $parts['query'] : ''; |
513
|
|
|
$hash = isset($parts['fragment']) ? $parts['fragment'] : ''; |
514
|
|
|
$uri = $this->baseUrl()->withPath($path)->withQuery($query)->withFragment($hash); |
515
|
|
|
} |
516
|
|
|
} |
517
|
|
|
} |
518
|
|
|
|
519
|
|
|
return $uri; |
520
|
|
|
} |
521
|
|
|
} |
522
|
|
|
|
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.