Completed
Pull Request — master (#9)
by
unknown
06:52 queued 04:16
created

AbstractWebTemplate::metaTitle()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.7666
c 0
b 0
f 0
cc 3
nc 4
nop 0
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()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $title of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
228
    {
229
        $context = $this->contextObject();
230
231
        $desc = null;
232
        if ($context instanceof MetatagInterface) {
233
            $desc = (string)$context['metaDescription'];
234
        }
235
236
        if (!$desc) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $desc of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $img of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $img of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $img of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \ArrayIterator($this->seoMetadata) of type object<ArrayIterator> is incompatible with the declared type array of property $seoMetadata.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $key of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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