Passed
Push — v3 ( 6c8e4b...9ade3c )
by Andrew
21:16 queued 14s
created

MetaContainers   F

Complexity

Total Complexity 145

Size/Duplication

Total Lines 1004
Duplicated Lines 0 %

Test Coverage

Coverage 3.42%

Importance

Changes 10
Bugs 7 Features 2
Metric Value
wmc 145
eloc 449
dl 0
loc 1004
ccs 16
cts 468
cp 0.0342
rs 2
c 10
b 7
f 2

23 Methods

Rating   Name   Duplication   Size   Complexity  
C renderContainersByType() 0 57 13
A getMetaItemByKey() 0 15 6
A invalidateCaches() 0 17 2
A parseGlobalVars() 0 23 3
A getHash() 0 10 3
F previewMetaContainers() 0 63 14
B renderContainersArrayByType() 0 33 9
B setMatchedElement() 0 31 10
F loadMetaContainers() 0 104 19
A invalidateContainerCacheById() 0 31 5
A getMatchedMetaBundle() 0 21 3
C loadFieldMetaContainers() 0 62 14
A addToMetaContainer() 0 7 2
A addMetaBundleToContainers() 0 42 3
A getContainersOfType() 0 11 3
A init() 0 7 2
A loadGlobalMetaContainers() 0 30 5
A includeMetaContainers() 0 32 5
B createMetaContainer() 0 37 9
A loadContentMetaContainers() 0 16 3
A invalidateContainerCacheByPath() 0 20 3
A getMetaContainer() 0 14 3
A getAllowedUrlParams() 0 22 6

How to fix   Complexity   

Complex Class

Complex classes like MetaContainers often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MetaContainers, and based on these observations, apply Extract Interface, too.

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\base\ElementInterface;
18
use craft\commerce\Plugin as CommercePlugin;
19
use craft\console\Application as ConsoleApplication;
20
use craft\elements\GlobalSet;
21
use craft\helpers\ElementHelper;
22
use craft\web\UrlManager;
23
use nystudio107\seomatic\base\MetaContainer;
24
use nystudio107\seomatic\base\MetaItem;
25
use nystudio107\seomatic\events\InvalidateContainerCachesEvent;
26
use nystudio107\seomatic\events\MetaBundleDebugDataEvent;
27
use nystudio107\seomatic\helpers\ArrayHelper;
28
use nystudio107\seomatic\helpers\DynamicMeta as DynamicMetaHelper;
29
use nystudio107\seomatic\helpers\Field as FieldHelper;
30
use nystudio107\seomatic\helpers\Json;
31
use nystudio107\seomatic\helpers\Localization as LocalizationHelper;
32
use nystudio107\seomatic\helpers\MetaValue as MetaValueHelper;
33
use nystudio107\seomatic\helpers\UrlHelper;
34
use nystudio107\seomatic\models\FrontendTemplateContainer;
35
use nystudio107\seomatic\models\MetaBundle;
36
use nystudio107\seomatic\models\MetaGlobalVars;
37
use nystudio107\seomatic\models\MetaJsonLd;
38
use nystudio107\seomatic\models\MetaJsonLdContainer;
39
use nystudio107\seomatic\models\MetaLinkContainer;
40
use nystudio107\seomatic\models\MetaScript;
41
use nystudio107\seomatic\models\MetaScriptContainer;
42
use nystudio107\seomatic\models\MetaSitemapVars;
43
use nystudio107\seomatic\models\MetaSiteVars;
44
use nystudio107\seomatic\models\MetaTagContainer;
45
use nystudio107\seomatic\models\MetaTitleContainer;
46
use nystudio107\seomatic\seoelements\SeoProduct;
47
use nystudio107\seomatic\Seomatic;
48
use nystudio107\seomatic\services\JsonLd as JsonLdService;
49
use nystudio107\seomatic\variables\SeomaticVariable;
50
use Throwable;
51
use yii\base\Exception;
52
use yii\base\InvalidConfigException;
53
use yii\caching\TagDependency;
54
use yii\web\BadRequestHttpException;
55
use function is_array;
56
use function is_object;
57
58
/**
59
 * Meta container functions for SEOmatic
60
 * An instance of the service is available via [[`Seomatic::$plugin->metaContainers`|`seomatic.containers`]]
61
 *
62
 * @author    nystudio107
63
 * @package   Seomatic
64
 * @since     3.0.0
65
 */
66
class MetaContainers extends Component
67
{
68
    // Constants
69
    // =========================================================================
70
71
    const GLOBAL_METACONTAINER_CACHE_TAG = 'seomatic_metacontainer';
72
    const METACONTAINER_CACHE_TAG = 'seomatic_metacontainer_';
73
74
    const CACHE_KEY = 'seomatic_metacontainer_';
75
    const INVALID_RESPONSE_CACHE_KEY = 'seomatic_invalid_response';
76
    const GLOBALS_CACHE_KEY = 'parsed_globals_';
77
    const SCRIPTS_CACHE_KEY = 'body_scripts_';
78
79
    /** @var array Rules for replacement values on arbitrary empty values */
80
    const COMPOSITE_SETTING_LOOKUP = [
81
        'ogImage' => [
82
            'metaBundleSettings.ogImageSource' => 'sameAsSeo.seoImage',
83
        ],
84
        'twitterImage' => [
85
            'metaBundleSettings.twitterImageSource' => 'sameAsSeo.seoImage',
86
        ],
87
    ];
88
89
    /**
90
     * @event InvalidateContainerCachesEvent The event that is triggered when SEOmatic
91
     *        is about to clear its meta container caches
92
     *
93
     * ---
94
     * ```php
95
     * use nystudio107\seomatic\events\InvalidateContainerCachesEvent;
96
     * use nystudio107\seomatic\services\MetaContainers;
97
     * use yii\base\Event;
98
     * Event::on(MetaContainers::class, MetaContainers::EVENT_INVALIDATE_CONTAINER_CACHES, function(InvalidateContainerCachesEvent $e) {
99
     *     // Container caches are about to be cleared
100
     * });
101
     * ```
102
     */
103
    const EVENT_INVALIDATE_CONTAINER_CACHES = 'invalidateContainerCaches';
104
105
    /**
106
     * @event MetaBundleDebugDataEvent The event that is triggered to record MetaBundle
107
     * debug data
108
     *
109
     * ---
110
     * ```php
111
     * use nystudio107\seomatic\events\MetaBundleDebugDataEvent;
112
     * use nystudio107\seomatic\services\MetaContainers;
113
     * use yii\base\Event;
114
     * Event::on(MetaContainers::class, MetaContainers::EVENT_METABUNDLE_DEBUG_DATA, function(MetaBundleDebugDataEvent $e) {
115
     *     // Do something with the MetaBundle debug data
116
     * });
117
     * ```
118
     */
119
    const EVENT_METABUNDLE_DEBUG_DATA = 'metaBundleDebugData';
120
121
    // Public Properties
122
    // =========================================================================
123
124
    /**
125
     * @var MetaGlobalVars|null
126
     */
127
    public $metaGlobalVars;
128
129
    /**
130
     * @var MetaSiteVars|null
131
     */
132
    public $metaSiteVars;
133
134
    /**
135
     * @var MetaSitemapVars|null
136
     */
137
    public $metaSitemapVars;
138
139
    /**
140
     * @var string The current page number of paginated pages
141
     */
142
    public $paginationPage = '1';
143
144
    /**
145
     * @var null|string Cached nonce to be shared by all JSON-LD entities
146
     */
147
    public $cachedJsonLdNonce;
148
149
    /**
150
     * @var MetaContainer[]|array|null
151
     */
152
    public $metaContainers = [];
153
154
    // Protected Properties
155
    // =========================================================================
156
157
    /**
158
     * @var null|MetaBundle
159
     */
160
    protected $matchedMetaBundle;
161
162
    /**
163
     * @var null|TagDependency
164
     */
165
    protected $containerDependency;
166
167
    /**
168
     * @var bool Whether or not the matched element should be included in the
169
     *      meta containers
170
     */
171
    protected $includeMatchedElement = true;
172
173
    // Public Methods
174
    // =========================================================================
175
176
    /**
177
     * @inheritdoc
178
     */
179 1
    public function init()
180
    {
181 1
        parent::init();
182
        // Get the page number of this request
183 1
        $request = Craft::$app->getRequest();
184 1
        if (!$request->isConsoleRequest) {
185
            $this->paginationPage = (string)$request->pageNum;
186
        }
187
    }
188
189
    /**
190
     * Return the containers of a specific type
191
     *
192
     * @param string $type
193
     *
194
     * @return array
195
     */
196
    public function getContainersOfType(string $type): array
197
    {
198
        $containers = [];
199
        /** @var MetaContainer $metaContainer */
200
        foreach ($this->metaContainers as $metaContainer) {
201
            if ($metaContainer::CONTAINER_TYPE === $type) {
202
                $containers[] = $metaContainer;
203
            }
204
        }
205
206
        return $containers;
207
    }
208
209
    /**
210
     * Include the meta containers
211
     */
212
    public function includeMetaContainers()
213
    {
214
        Craft::beginProfile('MetaContainers::includeMetaContainers', __METHOD__);
215
        // Fire an 'metaBundleDebugData' event
216
        if ($this->hasEventHandlers(self::EVENT_METABUNDLE_DEBUG_DATA)) {
217
            $metaBundle = new MetaBundle([
218
                'metaGlobalVars' => clone $this->metaGlobalVars,
219
                'metaSiteVars' => clone $this->metaSiteVars,
220
                'metaSitemapVars' => clone $this->metaSitemapVars,
221
                'metaContainers' => $this->metaContainers,
222
            ]);
223
            $event = new MetaBundleDebugDataEvent([
224
                'metaBundleCategory' => MetaBundleDebugDataEvent::COMBINED_META_BUNDLE,
225
                'metaBundle' => $metaBundle,
226
            ]);
227
            $this->trigger(self::EVENT_METABUNDLE_DEBUG_DATA, $event);
228
        }
229
        // Add in our http headers
230
        DynamicMetaHelper::includeHttpHeaders();
231
        DynamicMetaHelper::addCspTags();
232
        $this->parseGlobalVars();
233
        foreach ($this->metaContainers as $metaContainer) {
234
            /** @var $metaContainer MetaContainer */
235
            if ($metaContainer->include) {
236
                // Don't cache the rendered result if we're previewing meta containers
237
                if (Seomatic::$previewingMetaContainers) {
238
                    $metaContainer->clearCache = true;
239
                }
240
                $metaContainer->includeMetaData($this->containerDependency);
241
            }
242
        }
243
        Craft::endProfile('MetaContainers::includeMetaContainers', __METHOD__);
244
    }
245
246
    /**
247
     * Parse the global variables
248
     */
249
    public function parseGlobalVars()
250
    {
251
        $dependency = $this->containerDependency;
252
        $uniqueKey = $dependency->tags[3] ?? self::GLOBALS_CACHE_KEY;
253
        list($this->metaGlobalVars, $this->metaSiteVars) = Craft::$app->getCache()->getOrSet(
254
            self::GLOBALS_CACHE_KEY . $uniqueKey,
255
            function() use ($uniqueKey) {
256
                Craft::info(
257
                    self::GLOBALS_CACHE_KEY . ' cache miss: ' . $uniqueKey,
258
                    __METHOD__
259
                );
260
261
                if ($this->metaGlobalVars) {
262
                    $this->metaGlobalVars->parseProperties();
263
                }
264
                if ($this->metaSiteVars) {
265
                    $this->metaSiteVars->parseProperties();
266
                }
267
268
                return [$this->metaGlobalVars, $this->metaSiteVars];
269
            },
270
            Seomatic::$cacheDuration,
271
            $dependency
272
        );
273
    }
274
275
    /**
276
     * Prep all of the meta for preview purposes
277
     *
278
     * @param string $uri
279
     * @param int|null $siteId
280
     * @param bool $parseVariables Whether or not the variables should be
281
     *                                 parsed as Twig
282
     * @param bool $includeElement Whether or not the matched element
283
     *                                 should be factored into the preview
284
     */
285
    public function previewMetaContainers(
286
        string            $uri = '',
287
        int               $siteId = null,
288
        bool              $parseVariables = false,
289
        bool              $includeElement = true,
290
        ?ElementInterface $element = null
291
    ) {
292
        // If we've already previewed the containers for this request, there's no need to do it again
293
        if (Seomatic::$previewingMetaContainers && !Seomatic::$headlessRequest) {
294
            return;
295
        }
296
        // It's possible this won't exist at this point
297
        if (!Seomatic::$seomaticVariable) {
298
            // Create our variable and stash it in the plugin for global access
299
            Seomatic::$seomaticVariable = new SeomaticVariable();
300
        }
301
        Seomatic::$previewingMetaContainers = true;
302
        $this->includeMatchedElement = $includeElement;
303
        $this->loadMetaContainers($uri, $siteId, $element);
304
        // Load in the right globals
305
        $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...
306
        $globalSets = GlobalSet::findAll([
307
            'siteId' => $siteId,
308
        ]);
309
        foreach ($globalSets as $globalSet) {
310
            MetaValueHelper::$templatePreviewVars[$globalSet->handle] = $globalSet;
311
        }
312
        // Parse the global vars
313
        if ($parseVariables) {
314
            $this->parseGlobalVars();
315
        }
316
        // Get the homeUrl and canonicalUrl
317
        $homeUrl = '/';
318
        $canonicalUrl = $this->metaGlobalVars->parsedValue('canonicalUrl');
0 ignored issues
show
Bug introduced by
The method parsedValue() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

318
        /** @scrutinizer ignore-call */ 
319
        $canonicalUrl = $this->metaGlobalVars->parsedValue('canonicalUrl');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
319
        $canonicalUrl = DynamicMetaHelper::sanitizeUrl($canonicalUrl, false);
320
        // Special-case the global bundle
321
        if ($uri === MetaBundles::GLOBAL_META_BUNDLE || $uri === '__home__') {
322
            $canonicalUrl = '/';
323
        }
324
        try {
325
            $homeUrl = UrlHelper::siteUrl($homeUrl, null, null, $siteId);
326
            $canonicalUrl = UrlHelper::siteUrl($canonicalUrl, null, null, $siteId);
327
        } catch (Exception $e) {
328
            Craft::error($e->getMessage(), __METHOD__);
329
        }
330
        $canonical = Seomatic::$seomaticVariable->link->get('canonical');
331
        if ($canonical !== null) {
332
            $canonical->href = $canonicalUrl;
333
        }
334
        $home = Seomatic::$seomaticVariable->link->get('home');
335
        if ($home !== null) {
336
            $home->href = $homeUrl;
337
        }
338
        // The current language may _not_ match the current site, if we're headless
339
        $ogLocale = Seomatic::$seomaticVariable->tag->get('og:locale');
340
        if ($ogLocale !== null && $siteId !== null) {
341
            $site = Craft::$app->getSites()->getSiteById($siteId);
342
            if ($site !== null) {
343
                $ogLocale->content = LocalizationHelper::normalizeOgLocaleLanguage($site->language);
344
            }
345
        }
346
        // Update seomatic.meta.canonicalUrl when previewing meta containers
347
        $this->metaGlobalVars->canonicalUrl = $canonicalUrl;
348
    }
349
350
    /**
351
     * Load the meta containers
352
     *
353
     * @param string|null $uri
354
     * @param int|null $siteId
355
     * @param ElementInterface|null $element
356
     */
357
    public function loadMetaContainers(?string $uri = '', int $siteId = null, ?ElementInterface $element = null)
358
    {
359
        Craft::beginProfile('MetaContainers::loadMetaContainers', __METHOD__);
360
        // Avoid recursion
361
        if (!Seomatic::$loadingMetaContainers) {
362
            Seomatic::$loadingMetaContainers = true;
363
            $this->setMatchedElement($uri, $siteId);
0 ignored issues
show
Bug introduced by
It seems like $uri can also be of type null; however, parameter $uri of nystudio107\seomatic\ser...rs::setMatchedElement() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

363
            $this->setMatchedElement(/** @scrutinizer ignore-type */ $uri, $siteId);
Loading history...
364
            // If this is a draft or revision we're previewing, swap it in so they see the draft preview image & data
365
            if ($element && ElementHelper::isDraftOrRevision($element)) {
366
                Seomatic::setMatchedElement($element);
367
            }
368
            // Get the cache tag for the matched meta bundle
369
            $metaBundle = $this->getMatchedMetaBundle();
370
            $metaBundleSourceId = '';
371
            $metaBundleSourceType = '';
372
            if ($metaBundle) {
373
                $metaBundleSourceId = $metaBundle->sourceId;
374
                $metaBundleSourceType = $metaBundle->sourceBundleType;
375
            }
376
            // We need an actual $siteId here for the cache key
377
            if ($siteId === null) {
378
                $siteId = Craft::$app->getSites()->currentSite->id
379
                    ?? Craft::$app->getSites()->primarySite->id
380
                    ?? 1;
381
            }
382
            // Handle pagination
383
            $paginationPage = 'page' . $this->paginationPage;
384
            // Get the path for the current request
385
            $request = Craft::$app->getRequest();
386
            $requestPath = '/';
387
            if (!$request->getIsConsoleRequest()) {
388
                try {
389
                    $requestPath = $request->getPathInfo();
390
                } catch (InvalidConfigException $e) {
391
                    Craft::error($e->getMessage(), __METHOD__);
392
                }
393
                // If this is any type of a preview, ensure that it's not cached
394
                if (Seomatic::$plugin->helper::isPreview()) {
395
                    Seomatic::$previewingMetaContainers = true;
396
                }
397
            }
398
            // Cache requests that have a token associated with them separately
399
            $token = '';
400
            $request = Craft::$app->getRequest();
401
            if (!$request->isConsoleRequest) {
402
                try {
403
                    $token = $request->getToken() ?? '';
404
                } catch (BadRequestHttpException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
405
                }
406
            }
407
            // Get our cache key
408
            $cacheKey = $uri . $siteId . $paginationPage . $requestPath . $this->getAllowedUrlParams() . $token;
409
            // For requests with a status code of >= 400, use one cache key
410
            if (!$request->isConsoleRequest) {
411
                $response = Craft::$app->getResponse();
412
                if ($response->statusCode >= 400) {
413
                    $cacheKey = $siteId . self::INVALID_RESPONSE_CACHE_KEY . $response->statusCode;
414
                }
415
            }
416
            // Load the meta containers
417
            $dependency = new TagDependency([
418
                'tags' => [
419
                    self::GLOBAL_METACONTAINER_CACHE_TAG,
420
                    self::METACONTAINER_CACHE_TAG . $metaBundleSourceId . $metaBundleSourceType . $siteId,
421
                    self::METACONTAINER_CACHE_TAG . $uri . $siteId,
422
                    self::METACONTAINER_CACHE_TAG . $cacheKey,
423
                ],
424
            ]);
425
            $this->containerDependency = $dependency;
426
            $debugModule = Seomatic::$settings->enableDebugToolbarPanel ? Craft::$app->getModule('debug') : null;
427
            if (Seomatic::$previewingMetaContainers || $debugModule) {
428
                Seomatic::$plugin->frontendTemplates->loadFrontendTemplateContainers($siteId);
429
                $this->loadGlobalMetaContainers($siteId);
430
                $this->loadContentMetaContainers();
431
                $this->loadFieldMetaContainers();
432
                // We only need the dynamic data for headless requests
433
                if (Seomatic::$headlessRequest || Seomatic::$plugin->helper::isPreview() || $debugModule) {
434
                    DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
435
                }
436
            } else {
437
                $cache = Craft::$app->getCache();
438
                list($this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers) = $cache->getOrSet(
439
                    self::CACHE_KEY . $cacheKey,
440
                    function() use ($uri, $siteId) {
441
                        Craft::info(
442
                            'Meta container cache miss: ' . $uri . '/' . $siteId,
443
                            __METHOD__
444
                        );
445
                        $this->loadGlobalMetaContainers($siteId);
446
                        $this->loadContentMetaContainers();
447
                        $this->loadFieldMetaContainers();
448
                        DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
449
450
                        return [$this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers];
451
                    },
452
                    Seomatic::$cacheDuration,
453
                    $dependency
454
                );
455
            }
456
            Seomatic::$seomaticVariable->init();
0 ignored issues
show
Bug introduced by
The method init() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

456
            Seomatic::$seomaticVariable->/** @scrutinizer ignore-call */ 
457
                                         init();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
457
            MetaValueHelper::cache();
458
            Seomatic::$loadingMetaContainers = false;
459
        }
460
        Craft::endProfile('MetaContainers::loadMetaContainers', __METHOD__);
461
    }
462
463
    /**
464
     * Return the MetaBundle that corresponds with the Seomatic::$matchedElement
465
     *
466
     * @return null|MetaBundle
467
     */
468
    public function getMatchedMetaBundle()
469
    {
470
        $metaBundle = null;
471
        /** @var Element|null $element */
472
        $element = Seomatic::$matchedElement;
473
        if ($element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
474
            $sourceType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element);
475
            if ($sourceType) {
476
                list($sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId, $typeId)
477
                    = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($element);
478
                $metaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
479
                    $sourceType,
480
                    $sourceId,
481
                    $sourceSiteId,
482
                    $typeId
483
                );
484
            }
485
        }
486
        $this->matchedMetaBundle = $metaBundle;
487
488
        return $metaBundle;
489
    }
490
491
    /**
492
     * Load the global site meta containers
493
     *
494
     * @param int|null $siteId
495
     */
496
    public function loadGlobalMetaContainers(int $siteId = null)
497
    {
498
        Craft::beginProfile('MetaContainers::loadGlobalMetaContainers', __METHOD__);
499
        if ($siteId === null) {
500
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
501
        }
502
        $metaBundle = Seomatic::$plugin->metaBundles->getGlobalMetaBundle($siteId);
503
        if ($metaBundle) {
504
            // Fire an 'metaBundleDebugData' event
505
            if ($this->hasEventHandlers(self::EVENT_METABUNDLE_DEBUG_DATA)) {
506
                $event = new MetaBundleDebugDataEvent([
507
                    'metaBundleCategory' => MetaBundleDebugDataEvent::GLOBAL_META_BUNDLE,
508
                    'metaBundle' => $metaBundle,
509
                ]);
510
                $this->trigger(self::EVENT_METABUNDLE_DEBUG_DATA, $event);
511
            }
512
            // Meta global vars
513
            $this->metaGlobalVars = clone $metaBundle->metaGlobalVars;
514
            // Meta site vars
515
            $this->metaSiteVars = clone $metaBundle->metaSiteVars;
516
            // Meta sitemap vars
517
            $this->metaSitemapVars = clone $metaBundle->metaSitemapVars;
518
            // Language
519
            $this->metaGlobalVars->language = Seomatic::$language;
520
            // Meta containers
521
            foreach ($metaBundle->metaContainers as $key => $metaContainer) {
522
                $this->metaContainers[$key] = clone $metaContainer;
523
            }
524
        }
525
        Craft::endProfile('MetaContainers::loadGlobalMetaContainers', __METHOD__);
526
    }
527
528
    /**
529
     * Add the meta bundle to our existing meta containers, overwriting meta
530
     * items with the same key
531
     *
532
     * @param MetaBundle $metaBundle
533
     */
534
    public function addMetaBundleToContainers(MetaBundle $metaBundle)
535
    {
536
        // Ensure the variable is synced properly first
537
        Seomatic::$seomaticVariable->init();
538
        // Meta global vars
539
        $attributes = $metaBundle->metaGlobalVars->getAttributes();
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

539
        /** @scrutinizer ignore-call */ 
540
        $attributes = $metaBundle->metaGlobalVars->getAttributes();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
540
        // Parse the meta values so we can filter out any blank or empty attributes
541
        // So that they can fall back on the parent container
542
        $parsedAttributes = $attributes;
543
        MetaValueHelper::parseArray($parsedAttributes);
544
        $parsedAttributes = array_filter(
545
            $parsedAttributes,
546
            [ArrayHelper::class, 'preserveBools']
547
        );
548
        $attributes = array_intersect_key($attributes, $parsedAttributes);
549
        // Add the attributes in
550
        $attributes = array_filter(
551
            $attributes,
552
            [ArrayHelper::class, 'preserveBools']
553
        );
554
        $this->metaGlobalVars->setAttributes($attributes, false);
555
        // Meta site vars
556
        /*
557
         * Don't merge in the Site vars, since they are only editable on
558
         * a global basis. Otherwise stale data will be unable to be edited
559
        $attributes = $metaBundle->metaSiteVars->getAttributes();
560
        $attributes = array_filter($attributes);
561
        $this->metaSiteVars->setAttributes($attributes, false);
562
        */
563
        // Meta sitemap vars
564
        $attributes = $metaBundle->metaSitemapVars->getAttributes();
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

564
        /** @scrutinizer ignore-call */ 
565
        $attributes = $metaBundle->metaSitemapVars->getAttributes();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
565
        $attributes = array_filter(
566
            $attributes,
567
            [ArrayHelper::class, 'preserveBools']
568
        );
569
        $this->metaSitemapVars->setAttributes($attributes, false);
0 ignored issues
show
Bug introduced by
The method setAttributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

569
        $this->metaSitemapVars->/** @scrutinizer ignore-call */ 
570
                                setAttributes($attributes, false);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
570
        // Language
571
        $this->metaGlobalVars->language = Seomatic::$language;
572
        // Meta containers
573
        foreach ($metaBundle->metaContainers as $key => $metaContainer) {
574
            foreach ($metaContainer->data as $metaTag) {
575
                $this->addToMetaContainer($metaTag, $key);
576
            }
577
        }
578
    }
579
580
    /**
581
     * Add the passed in MetaItem to the MetaContainer indexed as $key
582
     *
583
     * @param $data MetaItem The MetaItem to add to the container
584
     * @param $key  string   The key to the container to add the data to
585
     */
586 1
    public function addToMetaContainer(MetaItem $data, string $key)
587
    {
588
        /** @var MetaContainer $container */
589 1
        $container = $this->getMetaContainer($key);
590
591 1
        if ($container !== null) {
592
            $container->addData($data, $data->key);
593
        }
594
    }
595
596
    /**
597
     * @param string $key
598
     *
599
     * @return mixed|null
600
     */
601 1
    public function getMetaContainer(string $key)
602
    {
603 1
        if (!$key || empty($this->metaContainers[$key])) {
604 1
            $error = Craft::t(
605 1
                'seomatic',
606 1
                'Meta container with key `{key}` does not exist.',
607 1
                ['key' => $key]
608 1
            );
609 1
            Craft::error($error, __METHOD__);
610
611 1
            return null;
612
        }
613
614
        return $this->metaContainers[$key];
615
    }
616
617
    /**
618
     * Create a MetaContainer of the given $type with the $key
619
     *
620
     * @param string $type
621
     * @param string $key
622
     *
623
     * @return null|MetaContainer
624
     */
625
    public function createMetaContainer(string $type, string $key): ?MetaContainer
626
    {
627
        /** @var MetaContainer $container */
628
        $container = null;
629
        if (empty($this->metaContainers[$key])) {
630
            /** @var string|null $className */
631
            $className = null;
632
            // Create a new container based on the type passed in
633
            switch ($type) {
634
                case MetaTagContainer::CONTAINER_TYPE:
635
                    $className = MetaTagContainer::class;
636
                    break;
637
                case MetaLinkContainer::CONTAINER_TYPE:
638
                    $className = MetaLinkContainer::class;
639
                    break;
640
                case MetaScriptContainer::CONTAINER_TYPE:
641
                    $className = MetaScriptContainer::class;
642
                    break;
643
                case MetaJsonLdContainer::CONTAINER_TYPE:
644
                    $className = MetaJsonLdContainer::class;
645
                    break;
646
                case MetaTitleContainer::CONTAINER_TYPE:
647
                    $className = MetaTitleContainer::class;
648
                    break;
649
                default:
650
                    break;
651
            }
652
            if ($className) {
653
                $container = $className::create();
654
                if ($container) {
655
                    $this->metaContainers[$key] = $container;
656
                }
657
            }
658
        }
659
660
        /** @var MetaContainer|null $container */
661
        return $container;
662
    }
663
664
    // Protected Methods
665
    // =========================================================================
666
667
    /**
668
     * Render the HTML of all MetaContainers of a specific $type
669
     *
670
     * @param string $type
671
     *
672
     * @return string
673
     */
674
    public function renderContainersByType(string $type): string
675
    {
676
        $html = '';
677
        // Special-case for requests for the FrontendTemplateContainer "container"
678
        if ($type === FrontendTemplateContainer::CONTAINER_TYPE) {
679
            $renderedTemplates = [];
680
            if (Seomatic::$plugin->frontendTemplates->frontendTemplateContainer['data'] ?? false) {
681
                $frontendTemplateContainers = Seomatic::$plugin->frontendTemplates->frontendTemplateContainer['data'];
682
                foreach ($frontendTemplateContainers as $name => $frontendTemplateContainer) {
683
                    if ($frontendTemplateContainer->include) {
684
                        $result = $frontendTemplateContainer->render([
685
                        ]);
686
                        $renderedTemplates[] = [$name => $result];
687
                    }
688
                }
689
            }
690
            $html .= Json::encode($renderedTemplates);
691
692
            return $html;
693
        }
694
        /** @var MetaContainer $metaContainer */
695
        foreach ($this->metaContainers as $metaContainer) {
696
            if ($metaContainer::CONTAINER_TYPE === $type && $metaContainer->include) {
697
                $result = $metaContainer->render([
698
                    'renderRaw' => true,
699
                    'renderScriptTags' => true,
700
                    'array' => true,
701
                ]);
702
                // Special case for script containers, because they can have body scripts too
703
                if ($metaContainer::CONTAINER_TYPE === MetaScriptContainer::CONTAINER_TYPE) {
704
                    $bodyScript = '';
705
                    /** @var MetaScriptContainer $metaContainer */
706
                    if ($metaContainer->prepForInclusion()) {
707
                        foreach ($metaContainer->data as $metaScript) {
708
                            /** @var MetaScript $metaScript */
709
                            if (!empty($metaScript->bodyTemplatePath)) {
710
                                $bodyScript .= $metaScript->renderBodyHtml();
711
                            }
712
                        }
713
                    }
714
715
                    $result = Json::encode([
716
                        'script' => $result,
717
                        'bodyScript' => $bodyScript,
718
                    ]);
719
                }
720
721
                $html .= $result;
722
            }
723
        }
724
        // Special-case for requests for the MetaSiteVars "container"
725
        if ($type === MetaSiteVars::CONTAINER_TYPE) {
726
            $result = Json::encode($this->metaSiteVars->toArray());
0 ignored issues
show
Bug introduced by
The method toArray() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

726
            $result = Json::encode($this->metaSiteVars->/** @scrutinizer ignore-call */ toArray());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
727
            $html .= $result;
728
        }
729
730
        return $html;
731
    }
732
733
    /**
734
     * Render the HTML of all MetaContainers of a specific $type as an array
735
     *
736
     * @param string $type
737
     *
738
     * @return array
739
     */
740
    public function renderContainersArrayByType(string $type): array
741
    {
742
        $htmlArray = [];
743
        // Special-case for requests for the FrontendTemplateContainer "container"
744
        if ($type === FrontendTemplateContainer::CONTAINER_TYPE) {
745
            $renderedTemplates = [];
746
            if (Seomatic::$plugin->frontendTemplates->frontendTemplateContainer['data'] ?? false) {
747
                $frontendTemplateContainers = Seomatic::$plugin->frontendTemplates->frontendTemplateContainer['data'];
748
                foreach ($frontendTemplateContainers as $name => $frontendTemplateContainer) {
749
                    if ($frontendTemplateContainer->include) {
750
                        $result = $frontendTemplateContainer->render([
751
                        ]);
752
                        $renderedTemplates[] = [$name => $result];
753
                    }
754
                }
755
            }
756
757
            return $renderedTemplates;
758
        }
759
        /** @var MetaContainer $metaContainer */
760
        foreach ($this->metaContainers as $metaContainer) {
761
            if ($metaContainer::CONTAINER_TYPE === $type && $metaContainer->include) {
762
                /** @noinspection SlowArrayOperationsInLoopInspection */
763
                $htmlArray = array_merge($htmlArray, $metaContainer->renderArray());
764
            }
765
        }
766
        // Special-case for requests for the MetaSiteVars "container"
767
        if ($type === MetaSiteVars::CONTAINER_TYPE) {
768
            $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...
769
            $htmlArray = array_merge($htmlArray, $this->metaSiteVars->toArray());
770
        }
771
772
        return $htmlArray;
773
    }
774
775
    /**
776
     * Return a MetaItem object by $key from container $type
777
     *
778
     * @param string $key
779
     * @param string $type
780
     *
781
     * @return null|MetaItem
782
     */
783
    public function getMetaItemByKey(string $key, string $type = '')
784
    {
785
        $metaItem = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $metaItem is dead and can be removed.
Loading history...
786
        /** @var MetaContainer $metaContainer */
787
        foreach ($this->metaContainers as $metaContainer) {
788
            if (($metaContainer::CONTAINER_TYPE === $type) || empty($type)) {
789
                foreach ($metaContainer->data as $metaItem) {
790
                    if ($key === $metaItem->key) {
791
                        return $metaItem;
792
                    }
793
                }
794
            }
795
        }
796
797
        return null;
798
    }
799
800
    /**
801
     * Invalidate all of the meta container caches
802
     */
803
    public function invalidateCaches()
804
    {
805
        $cache = Craft::$app->getCache();
806
        TagDependency::invalidate($cache, self::GLOBAL_METACONTAINER_CACHE_TAG);
807
        Craft::info(
808
            'All meta container caches cleared',
809
            __METHOD__
810
        );
811
        // Trigger an event to let other plugins/modules know we've cleared our caches
812
        $event = new InvalidateContainerCachesEvent([
813
            'uri' => null,
814
            'siteId' => null,
815
            'sourceId' => null,
816
            'sourceType' => null,
817
        ]);
818
        if (!Craft::$app instanceof ConsoleApplication) {
819
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
820
        }
821
    }
822
823
    /**
824
     * Invalidate a meta bundle cache
825
     *
826
     * @param int $sourceId
827
     * @param null|string $sourceType
828
     * @param null|int $siteId
829
     */
830
    public function invalidateContainerCacheById(int $sourceId, $sourceType = null, $siteId = null)
831
    {
832
        $metaBundleSourceId = '';
833
        if ($sourceId) {
834
            $metaBundleSourceId = $sourceId;
835
        }
836
        $metaBundleSourceType = '';
837
        if ($sourceType) {
838
            $metaBundleSourceType = $sourceType;
839
        }
840
        if ($siteId === null) {
841
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
842
        }
843
        $cache = Craft::$app->getCache();
844
        TagDependency::invalidate(
845
            $cache,
846
            self::METACONTAINER_CACHE_TAG . $metaBundleSourceId . $metaBundleSourceType . $siteId
847
        );
848
        Craft::info(
849
            'Meta bundle cache cleared: ' . $metaBundleSourceId . ' / ' . $metaBundleSourceType . ' / ' . $siteId,
850
            __METHOD__
851
        );
852
        // Trigger an event to let other plugins/modules know we've cleared our caches
853
        $event = new InvalidateContainerCachesEvent([
854
            'uri' => null,
855
            'siteId' => $siteId,
856
            'sourceId' => $sourceId,
857
            'sourceType' => $metaBundleSourceType,
858
        ]);
859
        if (!Craft::$app instanceof ConsoleApplication) {
860
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
861
        }
862
    }
863
864
    /**
865
     * Invalidate a meta bundle cache
866
     *
867
     * @param string $uri
868
     * @param null|int $siteId
869
     */
870
    public function invalidateContainerCacheByPath(string $uri, $siteId = null)
871
    {
872
        $cache = Craft::$app->getCache();
873
        if ($siteId === null) {
874
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
875
        }
876
        TagDependency::invalidate($cache, self::METACONTAINER_CACHE_TAG . $uri . $siteId);
877
        Craft::info(
878
            'Meta container cache cleared: ' . $uri . ' / ' . $siteId,
879
            __METHOD__
880
        );
881
        // Trigger an event to let other plugins/modules know we've cleared our caches
882
        $event = new InvalidateContainerCachesEvent([
883
            'uri' => $uri,
884
            'siteId' => $siteId,
885
            'sourceId' => null,
886
            'sourceType' => null,
887
        ]);
888
        if (!Craft::$app instanceof ConsoleApplication) {
889
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
890
        }
891
    }
892
893
    // Protected Methods
894
    // =========================================================================
895
896
    /**
897
     * Set the element that matches the $uri
898
     *
899
     * @param string $uri
900
     * @param int|null $siteId
901
     */
902
    protected function setMatchedElement(string $uri, int $siteId = null)
903
    {
904
        if ($siteId === null) {
905
            $siteId = Craft::$app->getSites()->currentSite->id
906
                ?? Craft::$app->getSites()->primarySite->id
907
                ?? 1;
908
        }
909
        $element = null;
910
        $uri = trim($uri, '/');
911
        /** @var Element $element */
912
        $enabledOnly = !Seomatic::$previewingMetaContainers;
913
        // Try to use Craft's matched element if looking for an enabled element, the current `siteId` is being used and
914
        // the current `uri` matches what was in the request
915
        $request = Craft::$app->getRequest();
916
        if ($enabledOnly && !$request->getIsConsoleRequest()) {
917
            try {
918
                if ($siteId === Craft::$app->getSites()->currentSite->id
919
                    && $request->getPathInfo() === $uri) {
920
                    /** @var UrlManager $urlManager */
921
                    $urlManager = Craft::$app->getUrlManager();
922
                    $element = $urlManager->getMatchedElement();
923
                }
924
            } catch (Throwable $e) {
925
                Craft::error($e->getMessage(), __METHOD__);
926
            }
927
        }
928
        if (!$element) {
929
            $element = Craft::$app->getElements()->getElementByUri($uri, $siteId, $enabledOnly);
930
        }
931
        if ($element && ($element->uri !== null)) {
932
            Seomatic::setMatchedElement($element);
933
        }
934
    }
935
936
    /**
937
     * Return as key/value pairs any allowed parameters in the request
938
     *
939
     * @return string
940
     */
941
    protected function getAllowedUrlParams(): string
942
    {
943
        $result = '';
944
        $allowedParams = Seomatic::$settings->allowedUrlParams;
945
        if (Craft::$app->getPlugins()->getPlugin(SeoProduct::REQUIRED_PLUGIN_HANDLE)) {
946
            $commerce = CommercePlugin::getInstance();
947
            if ($commerce !== null) {
948
                $allowedParams[] = 'variant';
949
            }
950
        }
951
        // Iterate through the allowed parameters, adding the key/value pair to the $result string as found
952
        $request = Craft::$app->getRequest();
953
        if (!$request->isConsoleRequest) {
954
            foreach ($allowedParams as $allowedParam) {
955
                $value = $request->getParam($allowedParam);
956
                if ($value !== null) {
957
                    $result .= "{$allowedParam}={$value}";
958
                }
959
            }
960
        }
961
962
        return $result;
963
    }
964
965
    /**
966
     * Load the meta containers specific to the matched meta bundle
967
     */
968
    protected function loadContentMetaContainers()
969
    {
970
        Craft::beginProfile('MetaContainers::loadContentMetaContainers', __METHOD__);
971
        $metaBundle = $this->getMatchedMetaBundle();
972
        if ($metaBundle) {
973
            // Fire an 'metaBundleDebugData' event
974
            if ($this->hasEventHandlers(self::EVENT_METABUNDLE_DEBUG_DATA)) {
975
                $event = new MetaBundleDebugDataEvent([
976
                    'metaBundleCategory' => MetaBundleDebugDataEvent::CONTENT_META_BUNDLE,
977
                    'metaBundle' => $metaBundle,
978
                ]);
979
                $this->trigger(self::EVENT_METABUNDLE_DEBUG_DATA, $event);
980
            }
981
            $this->addMetaBundleToContainers($metaBundle);
982
        }
983
        Craft::endProfile('MetaContainers::loadContentMetaContainers', __METHOD__);
984
    }
985
986
    /**
987
     * Load any meta containers in the current element
988
     */
989
    protected function loadFieldMetaContainers()
990
    {
991
        Craft::beginProfile('MetaContainers::loadFieldMetaContainers', __METHOD__);
992
        $element = Seomatic::$matchedElement;
993
        if ($element && $this->includeMatchedElement) {
994
            /** @var Element $element */
995
            $fieldHandles = FieldHelper::fieldsOfTypeFromElement($element, FieldHelper::SEO_SETTINGS_CLASS_KEY, true);
996
            foreach ($fieldHandles as $fieldHandle) {
997
                if (!empty($element->$fieldHandle)) {
998
                    /** @var MetaBundle $metaBundle */
999
                    $metaBundle = $element->$fieldHandle;
1000
                    Seomatic::$plugin->metaBundles->pruneFieldMetaBundleSettings($metaBundle, $fieldHandle);
1001
1002
                    // See which properties have to be overridden, because the parent bundle says so.
1003
                    foreach (self::COMPOSITE_SETTING_LOOKUP as $settingName => $rules) {
1004
                        if (empty($metaBundle->metaGlobalVars->{$settingName})) {
1005
                            $parentBundle = Seomatic::$plugin->metaBundles->getContentMetaBundleForElement($element);
1006
1007
                            foreach ($rules as $settingPath => $action) {
1008
                                list($container, $property) = explode('.', $settingPath);
1009
                                list($testValue, $sourceSetting) = explode('.', $action);
1010
1011
                                $bundleProp = $parentBundle->{$container}->{$property} ?? null;
1012
                                if ($bundleProp == $testValue) {
1013
                                    $metaBundle->metaGlobalVars->{$settingName} = $metaBundle->metaGlobalVars->{$sourceSetting};
1014
                                }
1015
                            }
1016
                        }
1017
                    }
1018
1019
                    // Handle re-creating the `mainEntityOfPage` so that the model injected into the
1020
                    // templates has the appropriate attributes
1021
                    $generalContainerKey = MetaJsonLdContainer::CONTAINER_TYPE . JsonLdService::GENERAL_HANDLE;
1022
                    $generalContainer = $this->metaContainers[$generalContainerKey];
1023
                    if (($generalContainer !== null) && !empty($generalContainer->data['mainEntityOfPage'])) {
1024
                        /** @var MetaJsonLd $jsonLdModel */
1025
                        $jsonLdModel = $generalContainer->data['mainEntityOfPage'];
1026
                        $config = $jsonLdModel->getAttributes();
1027
                        $schemaType = $metaBundle->metaGlobalVars->mainEntityOfPage ?? $config['type'] ?? null;
1028
                        // If the schemaType is '' we should fall back on whatever the mainEntityOfPage already is
1029
                        if (empty($schemaType)) {
1030
                            $schemaType = null;
1031
                        }
1032
                        if ($schemaType !== null) {
1033
                            $config['key'] = 'mainEntityOfPage';
1034
                            $schemaType = MetaValueHelper::parseString($schemaType);
1035
                            $generalContainer->data['mainEntityOfPage'] = MetaJsonLd::create($schemaType, $config);
1036
                        }
1037
                    }
1038
                    // Fire an 'metaBundleDebugData' event
1039
                    if ($this->hasEventHandlers(self::EVENT_METABUNDLE_DEBUG_DATA)) {
1040
                        $event = new MetaBundleDebugDataEvent([
1041
                            'metaBundleCategory' => MetaBundleDebugDataEvent::FIELD_META_BUNDLE,
1042
                            'metaBundle' => $metaBundle,
1043
                        ]);
1044
                        $this->trigger(self::EVENT_METABUNDLE_DEBUG_DATA, $event);
1045
                    }
1046
                    $this->addMetaBundleToContainers($metaBundle);
1047
                }
1048
            }
1049
        }
1050
        Craft::endProfile('MetaContainers::loadFieldMetaContainers', __METHOD__);
1051
    }
1052
1053
    /**
1054
     * Generate an md5 hash from an object or array
1055
     *
1056
     * @param string|array|MetaItem $data
1057
     *
1058
     * @return string
1059
     */
1060
    protected function getHash($data): string
1061
    {
1062
        if (is_object($data)) {
1063
            $data = $data->toArray();
1064
        }
1065
        if (is_array($data)) {
1066
            $data = serialize($data);
1067
        }
1068
1069
        return md5($data);
1070
    }
1071
1072
    // Private Methods
1073
    // =========================================================================
1074
}
1075