MetaContainers::previewMetaContainers()   F
last analyzed

Complexity

Conditions 14
Paths 577

Size

Total Lines 62
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 210

Importance

Changes 0
Metric Value
eloc 36
c 0
b 0
f 0
dl 0
loc 62
ccs 0
cts 37
cp 0
rs 2.6875
cc 14
nc 577
nop 4
crap 210

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

321
        /** @scrutinizer ignore-call */ 
322
        $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...
322
        $canonicalUrl = DynamicMetaHelper::sanitizeUrl($canonicalUrl, false);
323
        // Special-case the global bundle
324
        if ($uri === MetaBundles::GLOBAL_META_BUNDLE || $uri === '__home__') {
325
            $canonicalUrl = '/';
326
        }
327
        try {
328
            $homeUrl = UrlHelper::siteUrl($homeUrl, null, null, $siteId);
329
            $canonicalUrl = UrlHelper::siteUrl($canonicalUrl, null, null, $siteId);
330
        } catch (Exception $e) {
331
            Craft::error($e->getMessage(), __METHOD__);
332
        }
333
        $canonical = Seomatic::$seomaticVariable->link->get('canonical');
334
        if ($canonical !== null) {
335
            $canonical->href = $canonicalUrl;
336
        }
337
        $home = Seomatic::$seomaticVariable->link->get('home');
338
        if ($home !== null) {
339
            $home->href = $homeUrl;
340
        }
341
        // The current language may _not_ match the current site, if we're headless
342
        $ogLocale = Seomatic::$seomaticVariable->tag->get('og:locale');
343
        if ($ogLocale !== null && $siteId !== null) {
344
            $site = Craft::$app->getSites()->getSiteById($siteId);
345
            if ($site !== null) {
346
                $ogLocale->content = LocalizationHelper::normalizeOgLocaleLanguage($site->language);
347
            }
348
        }
349
        // Update seomatic.meta.canonicalUrl when previewing meta containers
350
        $this->metaGlobalVars->canonicalUrl = $canonicalUrl;
351
    }
352
353
    /**
354
     * Load the meta containers
355
     *
356
     * @param string|null $uri
357
     * @param int|null $siteId
358
     */
359
    public function loadMetaContainers(?string $uri = '', ?int $siteId = null)
360
    {
361
        Craft::beginProfile('MetaContainers::loadMetaContainers', __METHOD__);
362
        // Avoid recursion
363
        if (!Seomatic::$loadingMetaContainers) {
364
            Seomatic::$loadingMetaContainers = true;
365
            $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

365
            $this->setMatchedElement(/** @scrutinizer ignore-type */ $uri, $siteId);
Loading history...
366
            // Get the cache tag for the matched meta bundle
367
            $metaBundle = $this->getMatchedMetaBundle();
368
            $metaBundleSourceId = '';
369
            $metaBundleSourceType = '';
370
            if ($metaBundle) {
371
                $metaBundleSourceId = $metaBundle->sourceId;
372
                $metaBundleSourceType = $metaBundle->sourceBundleType;
373
            }
374
            // We need an actual $siteId here for the cache key
375
            if ($siteId === null) {
376
                $siteId = Craft::$app->getSites()->currentSite->id
377
                    ?? Craft::$app->getSites()->primarySite->id
378
                    ?? 1;
379
            }
380
            // Handle pagination
381
            $paginationPage = 'page' . $this->paginationPage;
382
            // Get the path for the current request
383
            $request = Craft::$app->getRequest();
384
            $requestPath = '/';
385
            if (!$request->getIsConsoleRequest()) {
386
                try {
387
                    $requestPath = $request->getPathInfo();
388
                } catch (InvalidConfigException $e) {
389
                    Craft::error($e->getMessage(), __METHOD__);
390
                }
391
                // If this is any type of a preview, ensure that it's not cached
392
                if (Seomatic::$plugin->helper::isPreview()) {
393
                    Seomatic::$previewingMetaContainers = true;
394
                }
395
            }
396
            // Get our cache key
397
            $cacheKey = $uri . $siteId . $paginationPage . $requestPath . $this->getAllowedUrlParams();
398
            // For requests with a status code of >= 400, use one cache key
399
            if (!$request->isConsoleRequest) {
400
                $response = Craft::$app->getResponse();
401
                if ($response->statusCode >= 400) {
402
                    $cacheKey = $siteId . self::INVALID_RESPONSE_CACHE_KEY . $response->statusCode;
403
                }
404
            }
405
            // Load the meta containers
406
            $dependency = new TagDependency([
407
                'tags' => [
408
                    self::GLOBAL_METACONTAINER_CACHE_TAG,
409
                    self::METACONTAINER_CACHE_TAG . $metaBundleSourceId . $metaBundleSourceType . $siteId,
410
                    self::METACONTAINER_CACHE_TAG . $uri . $siteId,
411
                    self::METACONTAINER_CACHE_TAG . $cacheKey,
412
                ],
413
            ]);
414
            $this->containerDependency = $dependency;
415
            $debugModule = Seomatic::$settings->enableDebugToolbarPanel ? Craft::$app->getModule('debug') : null;
416
            if (Seomatic::$previewingMetaContainers || $debugModule) {
417
                Seomatic::$plugin->frontendTemplates->loadFrontendTemplateContainers($siteId);
418
                $this->loadGlobalMetaContainers($siteId);
419
                $this->loadContentMetaContainers();
420
                $this->loadFieldMetaContainers();
421
                // We only need the dynamic data for headless requests
422
                if (Seomatic::$headlessRequest || Seomatic::$plugin->helper::isPreview() || $debugModule) {
423
                    DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
424
                }
425
            } else {
426
                $cache = Craft::$app->getCache();
427
                list($this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers) = $cache->getOrSet(
428
                    self::CACHE_KEY . $cacheKey,
429
                    function() use ($uri, $siteId) {
430
                        Craft::info(
431
                            'Meta container cache miss: ' . $uri . '/' . $siteId,
432
                            __METHOD__
433
                        );
434
                        $this->loadGlobalMetaContainers($siteId);
435
                        $this->loadContentMetaContainers();
436
                        $this->loadFieldMetaContainers();
437
                        DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
438
439
                        return [$this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers];
440
                    },
441
                    Seomatic::$cacheDuration,
442
                    $dependency
443
                );
444
            }
445
            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

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

528
        /** @scrutinizer ignore-call */ 
529
        $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...
529
        // Parse the meta values so we can filter out any blank or empty attributes
530
        // So that they can fall back on the parent container
531
        $parsedAttributes = $attributes;
532
        MetaValueHelper::parseArray($parsedAttributes);
533
        $parsedAttributes = array_filter(
534
            $parsedAttributes,
535
            [ArrayHelper::class, 'preserveBools']
536
        );
537
        $attributes = array_intersect_key($attributes, $parsedAttributes);
538
        // Add the attributes in
539
        $attributes = array_filter(
540
            $attributes,
541
            [ArrayHelper::class, 'preserveBools']
542
        );
543
        $this->metaGlobalVars->setAttributes($attributes, false);
544
        // Meta site vars
545
        /*
546
         * Don't merge in the Site vars, since they are only editable on
547
         * a global basis. Otherwise stale data will be unable to be edited
548
        $attributes = $metaBundle->metaSiteVars->getAttributes();
549
        $attributes = array_filter($attributes);
550
        $this->metaSiteVars->setAttributes($attributes, false);
551
        */
552
        // Meta sitemap vars
553
        $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

553
        /** @scrutinizer ignore-call */ 
554
        $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...
554
        $attributes = array_filter(
555
            $attributes,
556
            [ArrayHelper::class, 'preserveBools']
557
        );
558
        $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

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

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