Passed
Push — develop ( e44a16...15d822 )
by Andrew
08:40 queued 16s
created

MetaContainers::includeMetaContainers()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 13
c 2
b 0
f 0
nc 12
nop 0
dl 0
loc 25
ccs 0
cts 14
cp 0
crap 56
rs 8.8333
1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS 3.x
4
 *
5
 * A turnkey SEO implementation for Craft CMS that is comprehensive, powerful,
6
 * and flexible
7
 *
8
 * @link      https://nystudio107.com
9
 * @copyright Copyright (c) 2017 nystudio107
10
 */
11
12
namespace nystudio107\seomatic\services;
13
14
use nystudio107\seomatic\helpers\ArrayHelper;
15
use nystudio107\seomatic\helpers\Json;
16
use nystudio107\seomatic\models\MetaJsonLd;
17
use nystudio107\seomatic\Seomatic;
18
use nystudio107\seomatic\base\MetaContainer;
19
use nystudio107\seomatic\base\MetaItem;
20
use nystudio107\seomatic\events\InvalidateContainerCachesEvent;
21
use nystudio107\seomatic\helpers\DynamicMeta as DynamicMetaHelper;
22
use nystudio107\seomatic\helpers\Field as FieldHelper;
23
use nystudio107\seomatic\helpers\MetaValue as MetaValueHelper;
24
use nystudio107\seomatic\helpers\UrlHelper;
25
use nystudio107\seomatic\models\MetaBundle;
26
use nystudio107\seomatic\models\MetaGlobalVars;
27
use nystudio107\seomatic\models\MetaSiteVars;
28
use nystudio107\seomatic\models\MetaSitemapVars;
29
use nystudio107\seomatic\models\MetaJsonLdContainer;
30
use nystudio107\seomatic\models\MetaLinkContainer;
31
use nystudio107\seomatic\models\MetaScriptContainer;
32
use nystudio107\seomatic\models\MetaScript;
33
use nystudio107\seomatic\models\MetaTagContainer;
34
use nystudio107\seomatic\models\MetaTitleContainer;
35
use nystudio107\seomatic\services\JsonLd as JsonLdService;
36
use nystudio107\seomatic\variables\SeomaticVariable;
37
38
use Craft;
39
use craft\base\Component;
40
use craft\base\Element;
41
use craft\console\Application as ConsoleApplication;
42
use craft\elements\GlobalSet;
43
44
use yii\base\Exception;
45
use yii\base\InvalidConfigException;
46
use yii\caching\TagDependency;
47
48
/**
49
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
50
 * @package   Seomatic
51
 * @since     3.0.0
52
 */
53
class MetaContainers extends Component
54
{
55
    // Constants
56
    // =========================================================================
57
58
    const GLOBAL_METACONTAINER_CACHE_TAG = 'seomatic_metacontainer';
59
    const METACONTAINER_CACHE_TAG = 'seomatic_metacontainer_';
60
61
    const CACHE_KEY = 'seomatic_metacontainer_';
62
    const INVALID_RESPONSE_CACHE_KEY = 'seomatic_invalid_response';
63
    const GLOBALS_CACHE_KEY = 'parsed_globals_';
64
    const SCRIPTS_CACHE_KEY = 'body_scripts_';
65
66
    /**
67
     * @event InvalidateContainerCachesEvent The event that is triggered when SEOmatic
68
     *        is about to clear its meta container caches
69
     *
70
     * ---
71
     * ```php
72
     * use nystudio107\seomatic\events\InvalidateContainerCachesEvent;
73
     * use nystudio107\seomatic\services\MetaContainers;
74
     * use yii\base\Event;
75
     * Event::on(MetaContainers::class, MetaContainers::EVENT_INVALIDATE_CONTAINER_CACHES, function(InvalidateContainerCachesEvent $e) {
76
     *     // Container caches are about to be cleared
77
     * });
78
     * ```
79
     */
80
    const EVENT_INVALIDATE_CONTAINER_CACHES = 'invalidateContainerCaches';
81
82
    // Public Properties
83
    // =========================================================================
84
85
    /**
86
     * @var MetaGlobalVars
87
     */
88
    public $metaGlobalVars;
89
90
    /**
91
     * @var MetaSiteVars
92
     */
93
    public $metaSiteVars;
94
95
    /**
96
     * @var MetaSitemapVars
97
     */
98
    public $metaSitemapVars;
99
100
    /**
101
     * @var string The current page number of paginated pages
102
     */
103
    public $paginationPage = '1';
104
105
    /**
106
     * @var null|string Cached nonce to be shared by all JSON-LD entities
107
     */
108
    public $cachedJsonLdNonce;
109
110
    // Protected Properties
111
    // =========================================================================
112
113
    /**
114
     * @var MetaContainer
115
     */
116
    protected $metaContainers = [];
117
118
    /**
119
     * @var null|MetaBundle
120
     */
121
    protected $matchedMetaBundle;
122
123
    /**
124
     * @var null|TagDependency
125
     */
126
    protected $containerDependency;
127
128
    /**
129
     * @var bool Whether or not the matched element should be included in the
130
     *      meta containers
131
     */
132
    protected $includeMatchedElement = true;
133
134
    // Public Methods
135
    // =========================================================================
136
137
    /**
138
     * @inheritdoc
139
     */
140 1
    public function init()
141
    {
142 1
        parent::init();
143
        // Get the page number of this request
144 1
        $request = Craft::$app->getRequest();
145 1
        if (!$request->isConsoleRequest) {
146
            $this->paginationPage = (string)$request->pageNum;
147
        }
148 1
    }
149
150
    /**
151
     * Load the meta containers
152
     *
153
     * @param string|null $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
154
     * @param int|null    $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
155
     */
156
    public function loadMetaContainers(string $uri = '', int $siteId = null)
157
    {
158
        Craft::beginProfile('MetaContainers::loadMetaContainers', __METHOD__);
159
        // Avoid recursion
160
        if (!Seomatic::$loadingMetaContainers) {
161
            Seomatic::$loadingMetaContainers = true;
162
            $this->setMatchedElement($uri, $siteId);
163
            // Get the cache tag for the matched meta bundle
164
            $metaBundle = $this->getMatchedMetaBundle();
165
            $metaBundleSourceId = '';
166
            $metaBundleSourceType = '';
167
            if ($metaBundle) {
168
                $metaBundleSourceId = $metaBundle->sourceId;
169
                $metaBundleSourceType = $metaBundle->sourceBundleType;
170
            }
171
            // We need an actual $siteId here for the cache key
172
            if ($siteId === null) {
173
                $siteId = Craft::$app->getSites()->currentSite->id
174
                    ?? Craft::$app->getSites()->primarySite->id
175
                    ?? 1;
176
            }
177
            // Handle pagination
178
            $paginationPage = 'page'.$this->paginationPage;
179
            // Get the path for the current request
180
            $request = Craft::$app->getRequest();
181
            $requestPath = '/';
182
            if (!$request->getIsConsoleRequest()) {
183
                try {
184
                    $requestPath = $request->getPathInfo();
185
                } catch (InvalidConfigException $e) {
186
                    Craft::error($e->getMessage(), __METHOD__);
187
                }
188
                // If this is any type of a preview, ensure that it's not cached
189
                if (Seomatic::$plugin->helper::isPreview()) {
190
                    Seomatic::$previewingMetaContainers = true;
191
                }
192
            }
193
            // Get our cache key
194
            $cacheKey = $uri.$siteId.$paginationPage.$requestPath;
195
            // For requests with a status code of >= 400, use one cache key
196
            if (!$request->isConsoleRequest) {
197
                $response = Craft::$app->getResponse();
198
                if ($response->statusCode >= 400) {
199
                    $cacheKey = $siteId.self::INVALID_RESPONSE_CACHE_KEY.$response->statusCode;
200
                }
201
            }
202
            // Load the meta containers
203
            $dependency = new TagDependency([
204
                'tags' => [
205
                    self::GLOBAL_METACONTAINER_CACHE_TAG,
206
                    self::METACONTAINER_CACHE_TAG.$metaBundleSourceId.$metaBundleSourceType.$siteId,
207
                    self::METACONTAINER_CACHE_TAG.$uri.$siteId,
208
                    self::METACONTAINER_CACHE_TAG.$cacheKey,
209
                ],
210
            ]);
211
            $this->containerDependency = $dependency;
212
            if (Seomatic::$previewingMetaContainers) {
213
                $this->loadGlobalMetaContainers($siteId);
214
                $this->loadContentMetaContainers();
215
                $this->loadFieldMetaContainers();
216
                DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
217
            } else {
218
                $cache = Craft::$app->getCache();
219
                list($this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers) = $cache->getOrSet(
220
                    self::CACHE_KEY.$cacheKey,
221
                    function () use ($uri, $siteId) {
222
                        Craft::info(
223
                            'Meta container cache miss: '.$uri.'/'.$siteId,
224
                            __METHOD__
225
                        );
226
                        $this->loadGlobalMetaContainers($siteId);
227
                        $this->loadContentMetaContainers();
228
                        $this->loadFieldMetaContainers();
229
                        DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
230
231
                        return [$this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers];
232
                    },
233
                    Seomatic::$cacheDuration,
234
                    $dependency
235
                );
236
            }
237
            Seomatic::$seomaticVariable->init();
238
            MetaValueHelper::cache();
239
            Seomatic::$loadingMetaContainers = false;
240
        }
241
        Craft::endProfile('MetaContainers::loadMetaContainers', __METHOD__);
242
    }
243
244
    /**
245
     * Include any script body HTML
246
     *
247
     * @param int $bodyPosition
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
248
     */
249
    public function includeScriptBodyHtml(int $bodyPosition)
250
    {
251
        Craft::beginProfile('MetaContainers::includeScriptBodyHtml', __METHOD__);
252
        $dependency = $this->containerDependency;
253
        $uniqueKey = $dependency->tags[3] ?? self::GLOBALS_CACHE_KEY;
254
        $uniqueKey .= $bodyPosition;
255
        $scriptData = Craft::$app->getCache()->getOrSet(
256
            self::GLOBALS_CACHE_KEY.$uniqueKey,
257
            function () use ($uniqueKey, $bodyPosition) {
258
                Craft::info(
259
                    self::SCRIPTS_CACHE_KEY.' cache miss: '.$uniqueKey,
260
                    __METHOD__
261
                );
262
                $scriptData = [];
263
                $scriptContainers = $this->getContainersOfType(MetaScriptContainer::CONTAINER_TYPE);
264
                foreach ($scriptContainers as $scriptContainer) {
265
                    /** @var MetaScriptContainer $scriptContainer */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
266
                    if ($scriptContainer->prepForInclusion()) {
267
                        foreach ($scriptContainer->data as $metaScript) {
268
                            /** @var MetaScript $metaScript */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
269
                            if (!empty($metaScript->bodyTemplatePath)
270
                                && ((int)$metaScript->bodyPosition === $bodyPosition)) {
0 ignored issues
show
Coding Style introduced by
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
271
                                $scriptData[] = $metaScript->renderBodyHtml();
272
                            }
273
                        }
274
                    }
275
                }
276
277
                return $scriptData;
278
            },
279
            Seomatic::$cacheDuration,
280
            $dependency
281
        );
282
        // Output the script HTML
283
        foreach ($scriptData as $script) {
284
            if (is_string($script) && !empty($script)) {
285
                echo $script;
286
            }
287
        }
288
        Craft::endProfile('MetaContainers::includeScriptBodyHtml', __METHOD__);
289
    }
290
291
    /**
292
     * Include the meta containers
293
     */
294
    public function includeMetaContainers()
295
    {
296
        Craft::beginProfile('MetaContainers::includeMetaContainers', __METHOD__);
297
        // If this page is paginated, we need to factor that into the cache key
298
        // We also need to re-add the hreflangs
299
        if ($this->paginationPage !== '1') {
300
            if (Seomatic::$settings->addHrefLang && Seomatic::$settings->addPaginatedHreflang) {
301
                DynamicMetaHelper::addMetaLinkHrefLang();
302
            }
303
        }
304
        // Add in our http headers
305
        DynamicMetaHelper::includeHttpHeaders();
306
        DynamicMetaHelper::addCspTags();
307
        $this->parseGlobalVars();
308
        foreach ($this->metaContainers as $metaContainer) {
309
            /** @var $metaContainer MetaContainer */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
310
            if ($metaContainer->include) {
311
                // Don't cache the rendered result if we're previewing meta containers
312
                if (Seomatic::$previewingMetaContainers) {
313
                    $metaContainer->clearCache = true;
314
                }
315
                $metaContainer->includeMetaData($this->containerDependency);
316
            }
317
        }
318
        Craft::endProfile('MetaContainers::includeMetaContainers', __METHOD__);
319
    }
320
321
    /**
322
     * Parse the global variables
323
     */
324
    public function parseGlobalVars()
325
    {
326
        $dependency = $this->containerDependency;
327
        $uniqueKey = $dependency->tags[3] ?? self::GLOBALS_CACHE_KEY;
328
        list($this->metaGlobalVars, $this->metaSiteVars) = Craft::$app->getCache()->getOrSet(
329
            self::GLOBALS_CACHE_KEY.$uniqueKey,
330
            function () use ($uniqueKey) {
331
                Craft::info(
332
                    self::GLOBALS_CACHE_KEY.' cache miss: '.$uniqueKey,
333
                    __METHOD__
334
                );
335
336
                if ($this->metaGlobalVars) {
337
                    $this->metaGlobalVars->parseProperties();
338
                }
339
                if ($this->metaSiteVars) {
340
                    $this->metaSiteVars->parseProperties();
341
                }
342
343
                return [$this->metaGlobalVars, $this->metaSiteVars];
344
            },
345
            Seomatic::$cacheDuration,
346
            $dependency
347
        );
348
    }
349
350
    /**
351
     * Prep all of the meta for preview purposes
352
     *
353
     * @param string   $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
354
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
355
     * @param bool     $parseVariables Whether or not the variables should be
356
     *                                 parsed as Twig
357
     * @param bool     $includeElement Whether or not the matched element
358
     *                                 should be factored into the preview
359
     */
360
    public function previewMetaContainers(
361
        string $uri = '',
362
        int $siteId = null,
363
        bool $parseVariables = false,
364
        bool $includeElement = true
365
    ) {
366
        // It's possible this won't exist at this point
367
        if (!Seomatic::$seomaticVariable) {
368
            // Create our variable and stash it in the plugin for global access
369
            Seomatic::$seomaticVariable = new SeomaticVariable();
370
        }
371
        Seomatic::$previewingMetaContainers = true;
372
        $this->includeMatchedElement = $includeElement;
373
        $this->loadMetaContainers($uri, $siteId);
374
        // Load in the right globals
375
        $twig = Craft::$app->getView()->getTwig();
0 ignored issues
show
Unused Code introduced by
The assignment to $twig is dead and can be removed.
Loading history...
376
        $globalSets = GlobalSet::findAll([
377
            'siteId' => $siteId,
378
        ]);
379
        MetaValueHelper::$templatePreviewVars = [];
380
        foreach ($globalSets as $globalSet) {
381
            MetaValueHelper::$templatePreviewVars[$globalSet->handle] = $globalSet;
382
        }
383
        // Parse the global vars
384
        if ($parseVariables) {
385
            $this->parseGlobalVars();
386
        }
387
        // Get the homeUrl and canonicalUrl
388
        $homeUrl = '/';
389
        $canonicalUrl = DynamicMetaHelper::sanitizeUrl($uri, false);
390
        // Special-case the global bundle
391
        if ($uri === MetaBundles::GLOBAL_META_BUNDLE || $uri === '__home__') {
392
            $canonicalUrl = '/';
393
        }
394
        try {
395
            $homeUrl = UrlHelper::siteUrl($homeUrl, null, null, $siteId);
396
            $canonicalUrl = UrlHelper::siteUrl($canonicalUrl, null, null, $siteId);
397
        } catch (Exception $e) {
398
            Craft::error($e->getMessage(), __METHOD__);
399
        }
400
        $canonical = Seomatic::$seomaticVariable->link->get('canonical');
401
        if ($canonical !== null) {
402
            $canonical->href = $canonicalUrl;
403
        }
404
        $home = Seomatic::$seomaticVariable->link->get('home');
405
        if ($home !== null) {
406
            $home->href = $homeUrl;
407
        }
408
        // Update seomatic.meta.canonicalUrl when previewing meta containers
409
        $this->metaGlobalVars->canonicalUrl = $canonicalUrl;
410
    }
411
412
    /**
413
     * Add the passed in MetaItem to the MetaContainer indexed as $key
414
     *
415
     * @param $data MetaItem The MetaItem to add to the container
416
     * @param $key  string   The key to the container to add the data to
417
     */
418 1
    public function addToMetaContainer(MetaItem $data, string $key)
419
    {
420
        /** @var  $container MetaContainer */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
421 1
        $container = $this->getMetaContainer($key);
422
423 1
        if ($container !== null) {
424
            $container->addData($data, $data->key);
425
        }
426 1
    }
427
428
    /**
429
     * @param string $key
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
430
     *
431
     * @return mixed|null
432
     */
433 1
    public function getMetaContainer(string $key)
434
    {
435 1
        if (!$key || empty($this->metaContainers[$key])) {
436 1
            $error = Craft::t(
437 1
                'seomatic',
438 1
                'Meta container with key `{key}` does not exist.',
439 1
                ['key' => $key]
440
            );
441 1
            Craft::error($error, __METHOD__);
442
443 1
            return null;
444
        }
445
446
        return $this->metaContainers[$key];
447
    }
448
449
    /**
450
     * Create a MetaContainer of the given $type with the $key
451
     *
452
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
453
     * @param string $key
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
454
     *
455
     * @return null|MetaContainer
456
     */
457
    public function createMetaContainer(string $type, string $key): MetaContainer
458
    {
459
        /** @var MetaContainer $container */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
460
        $container = null;
461
        if (empty($this->metaContainers[$key])) {
462
            /** @var MetaContainer $className */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
463
            $className = null;
464
            // Create a new container based on the type passed in
465
            switch ($type) {
466
                case MetaTagContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
467
                    $className = MetaTagContainer::class;
468
                    break;
469
                case MetaLinkContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
470
                    $className = MetaLinkContainer::class;
471
                    break;
472
                case MetaScriptContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
473
                    $className = MetaScriptContainer::class;
474
                    break;
475
                case MetaJsonLdContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
476
                    $className = MetaJsonLdContainer::class;
477
                    break;
478
                case MetaTitleContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
479
                    $className = MetaTitleContainer::class;
480
                    break;
481
            }
482
            if ($className) {
483
                $container = $className::create();
484
                if ($container) {
0 ignored issues
show
introduced by
$container is of type nystudio107\seomatic\base\Container, thus it always evaluated to true.
Loading history...
485
                    $this->metaContainers[$key] = $container;
486
                }
487
            }
488
        }
489
490
        /** @var MetaContainer $className */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
491
        return $container;
492
    }
493
494
    /**
495
     * Return the containers of a specific type
496
     *
497
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
498
     *
499
     * @return array
500
     */
501
    public function getContainersOfType(string $type): array
502
    {
503
        $containers = [];
504
        /** @var  $metaContainer MetaContainer */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
505
        foreach ($this->metaContainers as $metaContainer) {
506
            if ($metaContainer::CONTAINER_TYPE === $type) {
507
                $containers[] = $metaContainer;
508
            }
509
        }
510
511
        return $containers;
512
    }
513
514
    /**
515
     * Render the HTML of all MetaContainers of a specific $type
516
     *
517
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
518
     *
519
     * @return string
520
     */
521
    public function renderContainersByType(string $type): string
522
    {
523
        $html = '';
524
        /** @var  $metaContainer MetaContainer */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
525
        foreach ($this->metaContainers as $metaContainer) {
526
            if ($metaContainer::CONTAINER_TYPE === $type && $metaContainer->include) {
527
                $result = $metaContainer->render([
528
                    'renderRaw'        => true,
529
                    'renderScriptTags' => true,
530
                    'array'            => true,
531
                ]);
532
                // Special case for script containers, because they can have body scripts too
533
                if ($metaContainer::CONTAINER_TYPE === MetaScriptContainer::CONTAINER_TYPE) {
534
                    $bodyScript = '';
535
                    /** @var MetaScriptContainer $metaContainer */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
536
                    if ($metaContainer->prepForInclusion()) {
537
                        foreach ($metaContainer->data as $metaScript) {
538
                            /** @var MetaScript $metaScript */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
539
                            if (!empty($metaScript->bodyTemplatePath)) {
540
                                $bodyScript .= $metaScript->renderBodyHtml();
541
                            }
542
                        }
543
                    }
544
545
                    $result = Json::encode([
546
                        'script' => $result,
547
                        'bodyScript' => $bodyScript,
548
                    ]);
549
                }
550
551
                $html .= $result;
552
            }
553
        }
554
        // Special-case for requests for the MetaSiteVars "container"
555
        if ($type === MetaSiteVars::CONTAINER_TYPE) {
556
            $result = Json::encode($this->metaSiteVars->toArray());
557
            $html .= $result;
558
        }
559
560
        return $html;
561
    }
562
563
    /**
564
     * Render the HTML of all MetaContainers of a specific $type as an array
565
     *
566
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
567
     *
568
     * @return array
569
     */
570
    public function renderContainersArrayByType(string $type): array
571
    {
572
        $htmlArray = [];
573
        /** @var  $metaContainer MetaContainer */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
574
        foreach ($this->metaContainers as $metaContainer) {
575
            if ($metaContainer::CONTAINER_TYPE === $type && $metaContainer->include) {
576
                /** @noinspection SlowArrayOperationsInLoopInspection */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
577
                $htmlArray = array_merge($htmlArray, $metaContainer->renderArray());
578
            }
579
        }
580
        // Special-case for requests for the MetaSiteVars "container"
581
        if ($type === MetaSiteVars::CONTAINER_TYPE) {
582
            $result = Json::encode($this->metaSiteVars->toArray());
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
583
            $htmlArray = array_merge($htmlArray, $this->metaSiteVars->toArray());
584
        }
585
586
        return $htmlArray;
587
    }
588
589
    /**
590
     * Return a MetaItem object by $key from container $type
591
     *
592
     * @param string $key
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
593
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
594
     *
595
     * @return null|MetaItem
596
     */
597
    public function getMetaItemByKey(string $key, string $type = '')
598
    {
599
        $metaItem = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $metaItem is dead and can be removed.
Loading history...
600
        /** @var  $metaContainer MetaContainer */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
601
        foreach ($this->metaContainers as $metaContainer) {
602
            if (($metaContainer::CONTAINER_TYPE === $type) || empty($type)) {
603
                /** @var  $metaTag MetaItem */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
604
                foreach ($metaContainer->data as $metaItem) {
605
                    if ($key === $metaItem->key) {
606
                        return $metaItem;
607
                    }
608
                }
609
            }
610
        }
611
612
        return null;
613
    }
614
615
    // Protected Methods
616
    // =========================================================================
617
618
    /**
619
     * Return the MetaBundle that corresponds with the Seomatic::$matchedElement
620
     *
621
     * @return null|MetaBundle
622
     */
623
    public function getMatchedMetaBundle()
624
    {
625
        $metaBundle = null;
626
        /** @var Element $element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
627
        $element = Seomatic::$matchedElement;
628
        if ($element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
629
            $sourceType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element);
630
            if ($sourceType) {
631
                list($sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId, $typeId)
632
                    = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($element);
633
                $metaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
634
                    $sourceType,
635
                    $sourceId,
636
                    $sourceSiteId,
637
                    $typeId
638
                );
639
            }
640
        }
641
        $this->matchedMetaBundle = $metaBundle;
642
643
        return $metaBundle;
644
    }
645
646
    /**
647
     * Add the meta bundle to our existing meta containers, overwriting meta
648
     * items with the same key
649
     *
650
     * @param MetaBundle $metaBundle
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
651
     */
652
    public function addMetaBundleToContainers(MetaBundle $metaBundle)
653
    {
654
        // Ensure the variable is synced properly first
655
        Seomatic::$seomaticVariable->init();
656
        // Meta global vars
657
        $attributes = $metaBundle->metaGlobalVars->getAttributes();
658
        // Parse the meta values so we can filter out any blank or empty attributes
659
        // So that they can fall back on the parent container
660
        $parsedAttributes = $attributes;
661
        MetaValueHelper::parseArray($parsedAttributes);
662
        $parsedAttributes = array_filter(
663
            $parsedAttributes,
664
            [ArrayHelper::class, 'preserveBools']
665
        );
666
        $attributes = array_intersect_key($attributes, $parsedAttributes);
667
        // Add the attributes in
668
        $attributes = array_filter(
669
            $attributes,
670
            [ArrayHelper::class, 'preserveBools']
671
        );
672
        $this->metaGlobalVars->setAttributes($attributes, false);
673
        // Meta site vars
674
        /*
675
         * Don't merge in the Site vars, since they are only editable on
676
         * a global basis. Otherwise stale data will be unable to be edited
677
        $attributes = $metaBundle->metaSiteVars->getAttributes();
678
        $attributes = array_filter($attributes);
679
        $this->metaSiteVars->setAttributes($attributes, false);
680
        */
681
        // Meta sitemap vars
682
        $attributes = $metaBundle->metaSitemapVars->getAttributes();
683
        $attributes = array_filter(
684
            $attributes,
685
            [ArrayHelper::class, 'preserveBools']
686
        );
687
        $this->metaSitemapVars->setAttributes($attributes, false);
688
        // Language
689
        $this->metaGlobalVars->language = Seomatic::$language;
690
        // Meta containers
691
        foreach ($metaBundle->metaContainers as $key => $metaContainer) {
692
            foreach ($metaContainer->data as $metaTag) {
693
                $this->addToMetaContainer($metaTag, $key);
694
            }
695
        }
696
    }
697
698
    /**
699
     * Invalidate all of the meta container caches
700
     */
701
    public function invalidateCaches()
702
    {
703
        $cache = Craft::$app->getCache();
704
        TagDependency::invalidate($cache, self::GLOBAL_METACONTAINER_CACHE_TAG);
705
        Craft::info(
706
            'All meta container caches cleared',
707
            __METHOD__
708
        );
709
        // Trigger an event to let other plugins/modules know we've cleared our caches
710
        $event = new InvalidateContainerCachesEvent([
711
            'uri' => null,
712
            'siteId' => null,
713
            'sourceId' => null,
714
            'sourceType' => null,
715
        ]);
716
        if (!Craft::$app instanceof ConsoleApplication) {
717
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
718
        }
719
    }
720
721
    /**
722
     * Invalidate a meta bundle cache
723
     *
724
     * @param int          $sourceId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 9 spaces after parameter type; 10 found
Loading history...
725
     * @param null|string  $sourceType
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 1 spaces after parameter type; 2 found
Loading history...
726
     * @param null|int     $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 4 spaces after parameter type; 5 found
Loading history...
727
     */
728
    public function invalidateContainerCacheById(int $sourceId, $sourceType = null, $siteId = null)
729
    {
730
        $metaBundleSourceId = '';
731
        if ($sourceId) {
732
            $metaBundleSourceId = $sourceId;
733
        }
734
        $metaBundleSourceType = '';
735
        if ($sourceType) {
736
            $metaBundleSourceType = $sourceType;
737
        }
738
        if ($siteId === null) {
739
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
740
        }
741
        $cache = Craft::$app->getCache();
742
        TagDependency::invalidate(
743
            $cache,
744
            self::METACONTAINER_CACHE_TAG.$metaBundleSourceId.$metaBundleSourceType.$siteId
745
        );
746
        Craft::info(
747
            'Meta bundle cache cleared: '.$metaBundleSourceId.' / '.$metaBundleSourceType.' / '.$siteId,
748
            __METHOD__
749
        );
750
        // Trigger an event to let other plugins/modules know we've cleared our caches
751
        $event = new InvalidateContainerCachesEvent([
752
            'uri' => null,
753
            'siteId' => $siteId,
754
            'sourceId' => $sourceId,
755
            'sourceType' => $metaBundleSourceType,
756
        ]);
757
        if (!Craft::$app instanceof ConsoleApplication) {
758
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
759
        }
760
    }
761
762
    /**
763
     * Invalidate a meta bundle cache
764
     *
765
     * @param string   $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
766
     * @param null|int $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
767
     */
768
    public function invalidateContainerCacheByPath(string $uri, $siteId = null)
769
    {
770
        $cache = Craft::$app->getCache();
771
        if ($siteId === null) {
772
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
773
        }
774
        TagDependency::invalidate($cache, self::METACONTAINER_CACHE_TAG.$uri.$siteId);
775
        Craft::info(
776
            'Meta container cache cleared: '.$uri.' / '.$siteId,
777
            __METHOD__
778
        );
779
        // Trigger an event to let other plugins/modules know we've cleared our caches
780
        $event = new InvalidateContainerCachesEvent([
781
            'uri' => $uri,
782
            'siteId' => $siteId,
783
            'sourceId' => null,
784
            'sourceType' => null,
785
        ]);
786
        if (!Craft::$app instanceof ConsoleApplication) {
787
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
788
        }
789
    }
790
791
    // Protected Methods
792
    // =========================================================================
793
794
    /**
795
     * Load the global site meta containers
796
     *
797
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
798
     */
799
    protected function loadGlobalMetaContainers(int $siteId = null)
800
    {
801
        Craft::beginProfile('MetaContainers::loadGlobalMetaContainers', __METHOD__);
802
        if ($siteId === null) {
803
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
804
        }
805
        $metaBundle = Seomatic::$plugin->metaBundles->getGlobalMetaBundle($siteId);
806
        if ($metaBundle) {
807
            // Meta global vars
808
            $this->metaGlobalVars = $metaBundle->metaGlobalVars;
809
            // Meta site vars
810
            $this->metaSiteVars = $metaBundle->metaSiteVars;
811
            // Meta sitemap vars
812
            $this->metaSitemapVars = $metaBundle->metaSitemapVars;
813
            // Language
814
            $this->metaGlobalVars->language = Seomatic::$language;
815
            // Meta containers
816
            foreach ($metaBundle->metaContainers as $key => $metaContainer) {
817
                $this->metaContainers[$key] = $metaContainer;
818
            }
819
        }
820
        Craft::beginProfile('MetaContainers::loadGlobalMetaContainers', __METHOD__);
821
    }
822
823
    /**
824
     * Load the meta containers specific to the matched meta bundle
825
     */
826
    protected function loadContentMetaContainers()
827
    {
828
        Craft::beginProfile('MetaContainers::loadContentMetaContainers', __METHOD__);
829
        $metaBundle = $this->getMatchedMetaBundle();
830
        if ($metaBundle) {
831
            $this->addMetaBundleToContainers($metaBundle);
832
        }
833
        Craft::beginProfile('MetaContainers::loadContentMetaContainers', __METHOD__);
834
    }
835
836
    /**
837
     * Load any meta containers in the current element
838
     */
839
    protected function loadFieldMetaContainers()
840
    {
841
        Craft::beginProfile('MetaContainers::loadFieldMetaContainers', __METHOD__);
842
        $element = Seomatic::$matchedElement;
843
        if ($element && $this->includeMatchedElement) {
844
            /** @var Element $element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
845
            $fieldHandles = FieldHelper::fieldsOfTypeFromElement($element, FieldHelper::SEO_SETTINGS_CLASS_KEY, true);
846
            foreach ($fieldHandles as $fieldHandle) {
847
                if (!empty($element->$fieldHandle)) {
848
                    /** @var MetaBundle $metaBundle */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
849
                    $metaBundle = $element->$fieldHandle;
850
                    Seomatic::$plugin->metaBundles->pruneFieldMetaBundleSettings($metaBundle, $fieldHandle);
851
                    // Handle re-creating the `mainEntityOfPage` so that the model injected into the
852
                    // templates has the appropriate attributes
853
                    $generalContainerKey = MetaJsonLdContainer::CONTAINER_TYPE.JsonLdService::GENERAL_HANDLE;
854
                    $generalContainer = $this->metaContainers[$generalContainerKey];
855
                    if (($generalContainer !== null) && !empty($generalContainer->data['mainEntityOfPage'])) {
856
                        /** @var MetaJsonLd $jsonLdModel */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
857
                        $jsonLdModel = $generalContainer->data['mainEntityOfPage'];
858
                        $config = $jsonLdModel->getAttributes();
859
                        $schemaType = $metaBundle->metaGlobalVars->mainEntityOfPage ?? $config['type'] ?? null;
860
                        // If the schemaType is '' we should fall back on whatever the mainEntityOfPage already is
861
                        if (empty($schemaType)) {
862
                            $schemaType = null;
863
                        }
864
                        if ($schemaType !== null) {
865
                            $config['key'] = 'mainEntityOfPage';
866
                            $schemaType = MetaValueHelper::parseString($schemaType);
867
                            $generalContainer->data['mainEntityOfPage'] = MetaJsonLd::create($schemaType, $config);
868
                        }
869
                    }
870
                    $this->addMetaBundleToContainers($metaBundle);
871
                }
872
            }
873
        }
874
        Craft::beginProfile('MetaContainers::loadFieldMetaContainers', __METHOD__);
875
    }
876
877
    /**
878
     * Set the element that matches the $uri
879
     *
880
     * @param string   $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
881
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
882
     */
883
    protected function setMatchedElement(string $uri, int $siteId = null)
884
    {
885
        if ($siteId === null) {
886
            $siteId = Craft::$app->getSites()->currentSite->id
887
                ?? Craft::$app->getSites()->primarySite->id
888
                ?? 1;
889
        }
890
        $uri = trim($uri, '/');
891
        /** @var Element $element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
892
        $enabledOnly = !Seomatic::$previewingMetaContainers;
893
        $element = Craft::$app->getElements()->getElementByUri($uri, $siteId, $enabledOnly);
894
        if ($element && ($element->uri !== null)) {
0 ignored issues
show
Bug introduced by
Accessing uri on the interface craft\base\ElementInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
895
            Seomatic::setMatchedElement($element);
896
        }
897
    }
898
899
    /**
900
     * Generate an md5 hash from an object or array
901
     *
902
     * @param string|array|MetaItem $data
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
903
     *
904
     * @return string
905
     */
906
    protected function getHash($data): string
907
    {
908
        if (\is_object($data)) {
909
            $data = $data->toArray();
910
        }
911
        if (\is_array($data)) {
912
            $data = serialize($data);
913
        }
914
915
        return md5($data);
916
    }
917
918
    // Private Methods
919
    // =========================================================================
920
}
921