Passed
Push — develop ( 7364aa...89429b )
by Andrew
09:18
created

MetaContainers::addToMetaContainer()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 2
dl 0
loc 7
ccs 3
cts 4
cp 0.75
crap 2.0625
rs 10
c 0
b 0
f 0
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 Craft;
15
use craft\base\Component;
16
use craft\base\Element;
17
use craft\commerce\Plugin as CommercePlugin;
0 ignored issues
show
Bug introduced by
The type craft\commerce\Plugin was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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