Passed
Push — v4 ( a4ddb7...ecf8fb )
by Andrew
44:32 queued 19:37
created

MetaContainers::includeMetaContainers()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 4
Bugs 2 Features 1
Metric Value
eloc 20
c 4
b 2
f 1
dl 0
loc 32
ccs 0
cts 23
cp 0
rs 9.2888
cc 5
nop 0
crap 30
nc 8
1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS
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;
0 ignored issues
show
Bug introduced by
The type craft\base\ElementInterface was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

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

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

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

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

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

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