Passed
Push — develop-v4 ( c90bff...1003e3 )
by Andrew
11:42
created

MetaContainers::includeScriptBodyHtml()   B

Complexity

Conditions 10
Paths 3

Size

Total Lines 42
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 26
dl 0
loc 42
ccs 0
cts 30
cp 0
rs 7.6666
c 1
b 1
f 0
cc 10
nc 3
nop 1
crap 110

1 Method

Rating   Name   Duplication   Size   Complexity  
A MetaContainers::getContainersOfType() 0 11 3

How to fix   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;
0 ignored issues
show
Bug introduced by
The type craft\commerce\Plugin was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
18
use craft\console\Application as ConsoleApplication;
19
use craft\elements\GlobalSet;
20
use nystudio107\seomatic\base\MetaContainer;
21
use nystudio107\seomatic\base\MetaItem;
22
use nystudio107\seomatic\events\InvalidateContainerCachesEvent;
23
use nystudio107\seomatic\helpers\ArrayHelper;
24
use nystudio107\seomatic\helpers\DynamicMeta as DynamicMetaHelper;
25
use nystudio107\seomatic\helpers\Field as FieldHelper;
26
use nystudio107\seomatic\helpers\Json;
27
use nystudio107\seomatic\helpers\Localization as LocalizationHelper;
28
use nystudio107\seomatic\helpers\MetaValue as MetaValueHelper;
29
use nystudio107\seomatic\helpers\UrlHelper;
30
use nystudio107\seomatic\models\FrontendTemplateContainer;
31
use nystudio107\seomatic\models\MetaBundle;
32
use nystudio107\seomatic\models\MetaGlobalVars;
33
use nystudio107\seomatic\models\MetaJsonLd;
34
use nystudio107\seomatic\models\MetaJsonLdContainer;
35
use nystudio107\seomatic\models\MetaLinkContainer;
36
use nystudio107\seomatic\models\MetaScript;
37
use nystudio107\seomatic\models\MetaScriptContainer;
38
use nystudio107\seomatic\models\MetaSitemapVars;
39
use nystudio107\seomatic\models\MetaSiteVars;
40
use nystudio107\seomatic\models\MetaTagContainer;
41
use nystudio107\seomatic\models\MetaTitleContainer;
42
use nystudio107\seomatic\seoelements\SeoProduct;
43
use nystudio107\seomatic\Seomatic;
44
use nystudio107\seomatic\services\JsonLd as JsonLdService;
45
use nystudio107\seomatic\variables\SeomaticVariable;
46
use yii\base\Exception;
47
use yii\base\InvalidConfigException;
48
use yii\caching\TagDependency;
49
use function is_array;
50
use function is_object;
51
52
/**
53
 * Meta container functions for SEOmatic
54
 * An instance of the service is available via [[`Seomatic::$plugin->metaContainers`|`seomatic.containers`]]
55
 *
56
 * @author    nystudio107
57
 * @package   Seomatic
58
 * @since     3.0.0
59
 */
60
class MetaContainers extends Component
61
{
62
    // Constants
63
    // =========================================================================
64
65
    const GLOBAL_METACONTAINER_CACHE_TAG = 'seomatic_metacontainer';
66
    const METACONTAINER_CACHE_TAG = 'seomatic_metacontainer_';
67
68
    const CACHE_KEY = 'seomatic_metacontainer_';
69
    const INVALID_RESPONSE_CACHE_KEY = 'seomatic_invalid_response';
70
    const GLOBALS_CACHE_KEY = 'parsed_globals_';
71
    const SCRIPTS_CACHE_KEY = 'body_scripts_';
72
73
    /** @var array Rules for replacement values on arbitrary empty values */
74
    const COMPOSITE_SETTING_LOOKUP = [
75
        'ogImage' => [
76
            'metaBundleSettings.ogImageSource' => 'sameAsSeo.seoImage',
77
        ],
78
        'twitterImage' => [
79
            'metaBundleSettings.twitterImageSource' => 'sameAsSeo.seoImage',
80
        ],
81
    ];
82
83
    /**
84
     * @event InvalidateContainerCachesEvent The event that is triggered when SEOmatic
85
     *        is about to clear its meta container caches
86
     *
87
     * ---
88
     * ```php
89
     * use nystudio107\seomatic\events\InvalidateContainerCachesEvent;
90
     * use nystudio107\seomatic\services\MetaContainers;
91
     * use yii\base\Event;
92
     * Event::on(MetaContainers::class, MetaContainers::EVENT_INVALIDATE_CONTAINER_CACHES, function(InvalidateContainerCachesEvent $e) {
93
     *     // Container caches are about to be cleared
94
     * });
95
     * ```
96
     */
97
    const EVENT_INVALIDATE_CONTAINER_CACHES = 'invalidateContainerCaches';
98
99
    // Public Properties
100
    // =========================================================================
101
102
    /**
103
     * @var MetaGlobalVars
104
     */
105
    public $metaGlobalVars;
106
107
    /**
108
     * @var MetaSiteVars
109
     */
110
    public $metaSiteVars;
111
112
    /**
113
     * @var MetaSitemapVars
114
     */
115
    public $metaSitemapVars;
116
117
    /**
118
     * @var string The current page number of paginated pages
119
     */
120
    public $paginationPage = '1';
121
122
    /**
123
     * @var null|string Cached nonce to be shared by all JSON-LD entities
124
     */
125
    public $cachedJsonLdNonce;
126
127
    // Protected Properties
128
    // =========================================================================
129
130
    /**
131
     * @var MetaContainer
132
     */
133
    protected $metaContainers = [];
134
135
    /**
136
     * @var null|MetaBundle
137
     */
138
    protected $matchedMetaBundle;
139
140
    /**
141
     * @var null|TagDependency
142
     */
143
    protected $containerDependency;
144
145
    /**
146
     * @var bool Whether or not the matched element should be included in the
147
     *      meta containers
148
     */
149
    protected $includeMatchedElement = true;
150
151
    // Public Methods
152
    // =========================================================================
153
154
    /**
155
     * @inheritdoc
156
     */
157 1
    public function init(): void
158
    {
159 1
        parent::init();
160
        // Get the page number of this request
161 1
        $request = Craft::$app->getRequest();
162 1
        if (!$request->isConsoleRequest) {
163
            $this->paginationPage = (string)$request->pageNum;
164
        }
165
    }
166
167
    /**
168
     * Return the containers of a specific type
169
     *
170
     * @param string $type
171
     *
172
     * @return array
173
     */
174
    public function getContainersOfType(string $type): array
175
    {
176
        $containers = [];
177
        /** @var  $metaContainer MetaContainer */
178
        foreach ($this->metaContainers as $metaContainer) {
179
            if ($metaContainer::CONTAINER_TYPE === $type) {
180
                $containers[] = $metaContainer;
181
            }
182
        }
183
184
        return $containers;
185
    }
186
187
    /**
188
     * Include the meta containers
189
     */
190
    public function includeMetaContainers()
191
    {
192
        Craft::beginProfile('MetaContainers::includeMetaContainers', __METHOD__);
193
        // If this page is paginated, we need to factor that into the cache key
194
        // We also need to re-add the hreflangs
195
        if ($this->paginationPage !== '1') {
196
            if (Seomatic::$settings->addHrefLang && Seomatic::$settings->addPaginatedHreflang) {
197
                DynamicMetaHelper::addMetaLinkHrefLang();
198
            }
199
        }
200
        // Add in our http headers
201
        DynamicMetaHelper::includeHttpHeaders();
202
        DynamicMetaHelper::addCspTags();
203
        $this->parseGlobalVars();
204
        foreach ($this->metaContainers as $metaContainer) {
205
            /** @var $metaContainer MetaContainer */
206
            if ($metaContainer->include) {
207
                // Don't cache the rendered result if we're previewing meta containers
208
                if (Seomatic::$previewingMetaContainers) {
209
                    $metaContainer->clearCache = true;
210
                }
211
                $metaContainer->includeMetaData($this->containerDependency);
212
            }
213
        }
214
        Craft::endProfile('MetaContainers::includeMetaContainers', __METHOD__);
215
    }
216
217
    /**
218
     * Parse the global variables
219
     */
220
    public function parseGlobalVars()
221
    {
222
        $dependency = $this->containerDependency;
223
        $uniqueKey = $dependency->tags[3] ?? self::GLOBALS_CACHE_KEY;
224
        list($this->metaGlobalVars, $this->metaSiteVars) = Craft::$app->getCache()->getOrSet(
225
            self::GLOBALS_CACHE_KEY . $uniqueKey,
226
            function () use ($uniqueKey) {
227
                Craft::info(
228
                    self::GLOBALS_CACHE_KEY . ' cache miss: ' . $uniqueKey,
229
                    __METHOD__
230
                );
231
232
                if ($this->metaGlobalVars) {
233
                    $this->metaGlobalVars->parseProperties();
234
                }
235
                if ($this->metaSiteVars) {
236
                    $this->metaSiteVars->parseProperties();
237
                }
238
239
                return [$this->metaGlobalVars, $this->metaSiteVars];
240
            },
241
            Seomatic::$cacheDuration,
242
            $dependency
243
        );
244
    }
245
246
    /**
247
     * Prep all of the meta for preview purposes
248
     *
249
     * @param string $uri
250
     * @param int|null $siteId
251
     * @param bool $parseVariables Whether or not the variables should be
252
     *                                 parsed as Twig
253
     * @param bool $includeElement Whether or not the matched element
254
     *                                 should be factored into the preview
255
     */
256
    public function previewMetaContainers(
257
        string $uri = '',
258
        int    $siteId = null,
259
        bool   $parseVariables = false,
260
        bool   $includeElement = true
261
    )
262
    {
263
        // If we've already previewed the containers for this request, there's no need to do it again
264
        if (Seomatic::$previewingMetaContainers && !Seomatic::$headlessRequest) {
265
            return;
266
        }
267
        // It's possible this won't exist at this point
268
        if (!Seomatic::$seomaticVariable) {
269
            // Create our variable and stash it in the plugin for global access
270
            Seomatic::$seomaticVariable = new SeomaticVariable();
271
        }
272
        Seomatic::$previewingMetaContainers = true;
273
        $this->includeMatchedElement = $includeElement;
274
        $this->loadMetaContainers($uri, $siteId);
275
        // Load in the right globals
276
        $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...
277
        $globalSets = GlobalSet::findAll([
278
            'siteId' => $siteId,
279
        ]);
280
        foreach ($globalSets as $globalSet) {
281
            MetaValueHelper::$templatePreviewVars[$globalSet->handle] = $globalSet;
282
        }
283
        // Parse the global vars
284
        if ($parseVariables) {
285
            $this->parseGlobalVars();
286
        }
287
        // Get the homeUrl and canonicalUrl
288
        $homeUrl = '/';
289
        $canonicalUrl = $this->metaGlobalVars->parsedValue('canonicalUrl');
290
        $canonicalUrl = DynamicMetaHelper::sanitizeUrl($canonicalUrl, false);
291
        // Special-case the global bundle
292
        if ($uri === MetaBundles::GLOBAL_META_BUNDLE || $uri === '__home__') {
293
            $canonicalUrl = '/';
294
        }
295
        try {
296
            $homeUrl = UrlHelper::siteUrl($homeUrl, null, null, $siteId);
297
            $canonicalUrl = UrlHelper::siteUrl($canonicalUrl, null, null, $siteId);
298
        } catch (Exception $e) {
299
            Craft::error($e->getMessage(), __METHOD__);
300
        }
301
        $canonical = Seomatic::$seomaticVariable->link->get('canonical');
302
        if ($canonical !== null) {
303
            $canonical->href = $canonicalUrl;
304
        }
305
        $home = Seomatic::$seomaticVariable->link->get('home');
306
        if ($home !== null) {
307
            $home->href = $homeUrl;
308
        }
309
        // The current language may _not_ match the current site, if we're headless
310
        $ogLocale = Seomatic::$seomaticVariable->tag->get('og:locale');
311
        if ($ogLocale !== null && $siteId !== null) {
312
            $site = Craft::$app->getSites()->getSiteById($siteId);
313
            if ($site !== null) {
314
                $ogLocale->content = LocalizationHelper::normalizeOgLocaleLanguage($site->language);
315
            }
316
        }
317
        // Update seomatic.meta.canonicalUrl when previewing meta containers
318
        $this->metaGlobalVars->canonicalUrl = $canonicalUrl;
319
    }
320
321
    /**
322
     * Load the meta containers
323
     *
324
     * @param string|null $uri
325
     * @param int|null $siteId
326
     */
327
    public function loadMetaContainers(string $uri = '', int $siteId = null)
328
    {
329
        Craft::beginProfile('MetaContainers::loadMetaContainers', __METHOD__);
330
        // Avoid recursion
331
        if (!Seomatic::$loadingMetaContainers) {
332
            Seomatic::$loadingMetaContainers = true;
333
            $this->setMatchedElement($uri, $siteId);
334
            // Get the cache tag for the matched meta bundle
335
            $metaBundle = $this->getMatchedMetaBundle();
336
            $metaBundleSourceId = '';
337
            $metaBundleSourceType = '';
338
            if ($metaBundle) {
339
                $metaBundleSourceId = $metaBundle->sourceId;
340
                $metaBundleSourceType = $metaBundle->sourceBundleType;
341
            }
342
            // We need an actual $siteId here for the cache key
343
            if ($siteId === null) {
344
                $siteId = Craft::$app->getSites()->currentSite->id
345
                    ?? Craft::$app->getSites()->primarySite->id
346
                    ?? 1;
347
            }
348
            // Handle pagination
349
            $paginationPage = 'page' . $this->paginationPage;
350
            // Get the path for the current request
351
            $request = Craft::$app->getRequest();
352
            $requestPath = '/';
353
            if (!$request->getIsConsoleRequest()) {
354
                try {
355
                    $requestPath = $request->getPathInfo();
356
                } catch (InvalidConfigException $e) {
357
                    Craft::error($e->getMessage(), __METHOD__);
358
                }
359
                // If this is any type of a preview, ensure that it's not cached
360
                if (Seomatic::$plugin->helper::isPreview()) {
361
                    Seomatic::$previewingMetaContainers = true;
362
                }
363
            }
364
            // Get our cache key
365
            $cacheKey = $uri . $siteId . $paginationPage . $requestPath . $this->getAllowedUrlParams();
366
            // For requests with a status code of >= 400, use one cache key
367
            if (!$request->isConsoleRequest) {
368
                $response = Craft::$app->getResponse();
369
                if ($response->statusCode >= 400) {
370
                    $cacheKey = $siteId . self::INVALID_RESPONSE_CACHE_KEY . $response->statusCode;
371
                }
372
            }
373
            // Load the meta containers
374
            $dependency = new TagDependency([
375
                'tags' => [
376
                    self::GLOBAL_METACONTAINER_CACHE_TAG,
377
                    self::METACONTAINER_CACHE_TAG . $metaBundleSourceId . $metaBundleSourceType . $siteId,
378
                    self::METACONTAINER_CACHE_TAG . $uri . $siteId,
379
                    self::METACONTAINER_CACHE_TAG . $cacheKey,
380
                ],
381
            ]);
382
            $this->containerDependency = $dependency;
383
            if (Seomatic::$previewingMetaContainers) {
384
                Seomatic::$plugin->frontendTemplates->loadFrontendTemplateContainers($siteId);
385
                $this->loadGlobalMetaContainers($siteId);
386
                $this->loadContentMetaContainers();
387
                $this->loadFieldMetaContainers();
388
                // We only need the dynamic data for headless requests
389
                if (Seomatic::$headlessRequest || Seomatic::$plugin->helper::isPreview()) {
390
                    DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
391
                }
392
            } else {
393
                $cache = Craft::$app->getCache();
394
                list($this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers) = $cache->getOrSet(
395
                    self::CACHE_KEY . $cacheKey,
396
                    function () use ($uri, $siteId) {
397
                        Craft::info(
398
                            'Meta container cache miss: ' . $uri . '/' . $siteId,
399
                            __METHOD__
400
                        );
401
                        $this->loadGlobalMetaContainers($siteId);
402
                        $this->loadContentMetaContainers();
403
                        $this->loadFieldMetaContainers();
404
                        DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
405
406
                        return [$this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers];
407
                    },
408
                    Seomatic::$cacheDuration,
409
                    $dependency
410
                );
411
            }
412
            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

412
            Seomatic::$seomaticVariable->/** @scrutinizer ignore-call */ 
413
                                         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...
413
            MetaValueHelper::cache();
414
            Seomatic::$loadingMetaContainers = false;
415
        }
416
        Craft::endProfile('MetaContainers::loadMetaContainers', __METHOD__);
417
    }
418
419
    /**
420
     * Return the MetaBundle that corresponds with the Seomatic::$matchedElement
421
     *
422
     * @return null|MetaBundle
423
     */
424
    public function getMatchedMetaBundle()
425
    {
426
        $metaBundle = null;
427
        /** @var Element $element */
428
        $element = Seomatic::$matchedElement;
429
        if ($element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
430
            $sourceType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element);
431
            if ($sourceType) {
432
                list($sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId, $typeId)
433
                    = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($element);
434
                $metaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
435
                    $sourceType,
436
                    $sourceId,
437
                    $sourceSiteId,
438
                    $typeId
439
                );
440
            }
441
        }
442
        $this->matchedMetaBundle = $metaBundle;
443
444
        return $metaBundle;
445
    }
446
447
    /**
448
     * Load the global site meta containers
449
     *
450
     * @param int|null $siteId
451
     */
452
    public function loadGlobalMetaContainers(int $siteId = null)
453
    {
454
        Craft::beginProfile('MetaContainers::loadGlobalMetaContainers', __METHOD__);
455
        if ($siteId === null) {
456
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
457
        }
458
        $metaBundle = Seomatic::$plugin->metaBundles->getGlobalMetaBundle($siteId);
459
        if ($metaBundle) {
460
            // Meta global vars
461
            $this->metaGlobalVars = clone $metaBundle->metaGlobalVars;
462
            // Meta site vars
463
            $this->metaSiteVars = clone $metaBundle->metaSiteVars;
464
            // Meta sitemap vars
465
            $this->metaSitemapVars = clone $metaBundle->metaSitemapVars;
466
            // Language
467
            $this->metaGlobalVars->language = Seomatic::$language;
468
            // Meta containers
469
            foreach ($metaBundle->metaContainers as $key => $metaContainer) {
470
                $this->metaContainers[$key] = clone $metaContainer;
471
            }
472
        }
473
        Craft::endProfile('MetaContainers::loadGlobalMetaContainers', __METHOD__);
474
    }
475
476
    /**
477
     * Add the meta bundle to our existing meta containers, overwriting meta
478
     * items with the same key
479
     *
480
     * @param MetaBundle $metaBundle
481
     */
482
    public function addMetaBundleToContainers(MetaBundle $metaBundle)
483
    {
484
        // Ensure the variable is synced properly first
485
        Seomatic::$seomaticVariable->init();
486
        // Meta global vars
487
        $attributes = $metaBundle->metaGlobalVars->getAttributes();
488
        // Parse the meta values so we can filter out any blank or empty attributes
489
        // So that they can fall back on the parent container
490
        $parsedAttributes = $attributes;
491
        MetaValueHelper::parseArray($parsedAttributes);
492
        $parsedAttributes = array_filter(
493
            $parsedAttributes,
494
            [ArrayHelper::class, 'preserveBools']
495
        );
496
        $attributes = array_intersect_key($attributes, $parsedAttributes);
497
        // Add the attributes in
498
        $attributes = array_filter(
499
            $attributes,
500
            [ArrayHelper::class, 'preserveBools']
501
        );
502
        $this->metaGlobalVars->setAttributes($attributes, false);
503
        // Meta site vars
504
        /*
505
         * Don't merge in the Site vars, since they are only editable on
506
         * a global basis. Otherwise stale data will be unable to be edited
507
        $attributes = $metaBundle->metaSiteVars->getAttributes();
508
        $attributes = array_filter($attributes);
509
        $this->metaSiteVars->setAttributes($attributes, false);
510
        */
511
        // Meta sitemap vars
512
        $attributes = $metaBundle->metaSitemapVars->getAttributes();
513
        $attributes = array_filter(
514
            $attributes,
515
            [ArrayHelper::class, 'preserveBools']
516
        );
517
        $this->metaSitemapVars->setAttributes($attributes, false);
518
        // Language
519
        $this->metaGlobalVars->language = Seomatic::$language;
520
        // Meta containers
521
        foreach ($metaBundle->metaContainers as $key => $metaContainer) {
522
            foreach ($metaContainer->data as $metaTag) {
523
                $this->addToMetaContainer($metaTag, $key);
524
            }
525
        }
526
    }
527
528
    /**
529
     * Add the passed in MetaItem to the MetaContainer indexed as $key
530
     *
531
     * @param $data MetaItem The MetaItem to add to the container
532
     * @param $key  string   The key to the container to add the data to
533
     */
534 1
    public function addToMetaContainer(MetaItem $data, string $key)
535
    {
536
        /** @var  $container MetaContainer */
537 1
        $container = $this->getMetaContainer($key);
538
539 1
        if ($container !== null) {
540
            $container->addData($data, $data->key);
541
        }
542
    }
543
544
    /**
545
     * @param string $key
546
     *
547
     * @return mixed|null
548
     */
549 1
    public function getMetaContainer(string $key)
550
    {
551 1
        if (!$key || empty($this->metaContainers[$key])) {
552 1
            $error = Craft::t(
553 1
                'seomatic',
554 1
                'Meta container with key `{key}` does not exist.',
555 1
                ['key' => $key]
556 1
            );
557 1
            Craft::error($error, __METHOD__);
558
559 1
            return null;
560
        }
561
562
        return $this->metaContainers[$key];
563
    }
564
565
    /**
566
     * Create a MetaContainer of the given $type with the $key
567
     *
568
     * @param string $type
569
     * @param string $key
570
     *
571
     * @return null|MetaContainer
572
     */
573
    public function createMetaContainer(string $type, string $key): MetaContainer
574
    {
575
        /** @var MetaContainer $container */
576
        $container = null;
577
        if (empty($this->metaContainers[$key])) {
578
            /** @var MetaContainer $className */
579
            $className = null;
580
            // Create a new container based on the type passed in
581
            switch ($type) {
582
                case MetaTagContainer::CONTAINER_TYPE:
583
                    $className = MetaTagContainer::class;
584
                    break;
585
                case MetaLinkContainer::CONTAINER_TYPE:
586
                    $className = MetaLinkContainer::class;
587
                    break;
588
                case MetaScriptContainer::CONTAINER_TYPE:
589
                    $className = MetaScriptContainer::class;
590
                    break;
591
                case MetaJsonLdContainer::CONTAINER_TYPE:
592
                    $className = MetaJsonLdContainer::class;
593
                    break;
594
                case MetaTitleContainer::CONTAINER_TYPE:
595
                    $className = MetaTitleContainer::class;
596
                    break;
597
            }
598
            if ($className) {
599
                $container = $className::create();
600
                if ($container) {
0 ignored issues
show
introduced by
$container is of type nystudio107\seomatic\base\Container, thus it always evaluated to true.
Loading history...
601
                    $this->metaContainers[$key] = $container;
602
                }
603
            }
604
        }
605
606
        /** @var MetaContainer $className */
607
        return $container;
608
    }
609
610
    // Protected Methods
611
    // =========================================================================
612
613
    /**
614
     * Render the HTML of all MetaContainers of a specific $type
615
     *
616
     * @param string $type
617
     *
618
     * @return string
619
     */
620
    public function renderContainersByType(string $type): string
621
    {
622
        $html = '';
623
        // Special-case for requests for the FrontendTemplateContainer "container"
624
        if ($type === FrontendTemplateContainer::CONTAINER_TYPE) {
625
            $renderedTemplates = [];
626
            if (Seomatic::$plugin->frontendTemplates->frontendTemplateContainer['data'] ?? false) {
627
                $frontendTemplateContainers = Seomatic::$plugin->frontendTemplates->frontendTemplateContainer['data'];
628
                foreach ($frontendTemplateContainers as $name => $frontendTemplateContainer) {
629
                    if ($frontendTemplateContainer->include) {
630
                        $result = $frontendTemplateContainer->render([
631
                        ]);
632
                        $renderedTemplates[] = [$name => $result];
633
                    }
634
                }
635
            }
636
            $html .= Json::encode($renderedTemplates);
637
638
            return $html;
639
        }
640
        /** @var  $metaContainer MetaContainer */
641
        foreach ($this->metaContainers as $metaContainer) {
642
            if ($metaContainer::CONTAINER_TYPE === $type && $metaContainer->include) {
643
                $result = $metaContainer->render([
644
                    'renderRaw' => true,
645
                    'renderScriptTags' => true,
646
                    'array' => true,
647
                ]);
648
                // Special case for script containers, because they can have body scripts too
649
                if ($metaContainer::CONTAINER_TYPE === MetaScriptContainer::CONTAINER_TYPE) {
650
                    $bodyScript = '';
651
                    /** @var MetaScriptContainer $metaContainer */
652
                    if ($metaContainer->prepForInclusion()) {
653
                        foreach ($metaContainer->data as $metaScript) {
654
                            /** @var MetaScript $metaScript */
655
                            if (!empty($metaScript->bodyTemplatePath)) {
656
                                $bodyScript .= $metaScript->renderBodyHtml();
657
                            }
658
                        }
659
                    }
660
661
                    $result = Json::encode([
662
                        'script' => $result,
663
                        'bodyScript' => $bodyScript,
664
                    ]);
665
                }
666
667
                $html .= $result;
668
            }
669
        }
670
        // Special-case for requests for the MetaSiteVars "container"
671
        if ($type === MetaSiteVars::CONTAINER_TYPE) {
672
            $result = Json::encode($this->metaSiteVars->toArray());
673
            $html .= $result;
674
        }
675
676
        return $html;
677
    }
678
679
    /**
680
     * Render the HTML of all MetaContainers of a specific $type as an array
681
     *
682
     * @param string $type
683
     *
684
     * @return array
685
     */
686
    public function renderContainersArrayByType(string $type): array
687
    {
688
        $htmlArray = [];
689
        // Special-case for requests for the FrontendTemplateContainer "container"
690
        if ($type === FrontendTemplateContainer::CONTAINER_TYPE) {
691
            $renderedTemplates = [];
692
            if (Seomatic::$plugin->frontendTemplates->frontendTemplateContainer['data'] ?? false) {
693
                $frontendTemplateContainers = Seomatic::$plugin->frontendTemplates->frontendTemplateContainer['data'];
694
                foreach ($frontendTemplateContainers as $name => $frontendTemplateContainer) {
695
                    if ($frontendTemplateContainer->include) {
696
                        $result = $frontendTemplateContainer->render([
697
                        ]);
698
                        $renderedTemplates[] = [$name => $result];
699
                    }
700
                }
701
            }
702
703
            return $renderedTemplates;
704
        }
705
        /** @var  $metaContainer MetaContainer */
706
        foreach ($this->metaContainers as $metaContainer) {
707
            if ($metaContainer::CONTAINER_TYPE === $type && $metaContainer->include) {
708
                /** @noinspection SlowArrayOperationsInLoopInspection */
709
                $htmlArray = array_merge($htmlArray, $metaContainer->renderArray());
710
            }
711
        }
712
        // Special-case for requests for the MetaSiteVars "container"
713
        if ($type === MetaSiteVars::CONTAINER_TYPE) {
714
            $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...
715
            $htmlArray = array_merge($htmlArray, $this->metaSiteVars->toArray());
716
        }
717
718
        return $htmlArray;
719
    }
720
721
    /**
722
     * Return a MetaItem object by $key from container $type
723
     *
724
     * @param string $key
725
     * @param string $type
726
     *
727
     * @return null|MetaItem
728
     */
729
    public function getMetaItemByKey(string $key, string $type = '')
730
    {
731
        $metaItem = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $metaItem is dead and can be removed.
Loading history...
732
        /** @var  $metaContainer MetaContainer */
733
        foreach ($this->metaContainers as $metaContainer) {
734
            if (($metaContainer::CONTAINER_TYPE === $type) || empty($type)) {
735
                /** @var  $metaTag MetaItem */
736
                foreach ($metaContainer->data as $metaItem) {
737
                    if ($key === $metaItem->key) {
738
                        return $metaItem;
739
                    }
740
                }
741
            }
742
        }
743
744
        return null;
745
    }
746
747
    /**
748
     * Invalidate all of the meta container caches
749
     */
750
    public function invalidateCaches()
751
    {
752
        $cache = Craft::$app->getCache();
753
        TagDependency::invalidate($cache, self::GLOBAL_METACONTAINER_CACHE_TAG);
754
        Craft::info(
755
            'All meta container caches cleared',
756
            __METHOD__
757
        );
758
        // Trigger an event to let other plugins/modules know we've cleared our caches
759
        $event = new InvalidateContainerCachesEvent([
760
            'uri' => null,
761
            'siteId' => null,
762
            'sourceId' => null,
763
            'sourceType' => null,
764
        ]);
765
        if (!Craft::$app instanceof ConsoleApplication) {
766
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
767
        }
768
    }
769
770
    /**
771
     * Invalidate a meta bundle cache
772
     *
773
     * @param int $sourceId
774
     * @param null|string $sourceType
775
     * @param null|int $siteId
776
     */
777
    public function invalidateContainerCacheById(int $sourceId, $sourceType = null, $siteId = null)
778
    {
779
        $metaBundleSourceId = '';
780
        if ($sourceId) {
781
            $metaBundleSourceId = $sourceId;
782
        }
783
        $metaBundleSourceType = '';
784
        if ($sourceType) {
785
            $metaBundleSourceType = $sourceType;
786
        }
787
        if ($siteId === null) {
788
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
789
        }
790
        $cache = Craft::$app->getCache();
791
        TagDependency::invalidate(
792
            $cache,
793
            self::METACONTAINER_CACHE_TAG . $metaBundleSourceId . $metaBundleSourceType . $siteId
794
        );
795
        Craft::info(
796
            'Meta bundle cache cleared: ' . $metaBundleSourceId . ' / ' . $metaBundleSourceType . ' / ' . $siteId,
797
            __METHOD__
798
        );
799
        // Trigger an event to let other plugins/modules know we've cleared our caches
800
        $event = new InvalidateContainerCachesEvent([
801
            'uri' => null,
802
            'siteId' => $siteId,
803
            'sourceId' => $sourceId,
804
            'sourceType' => $metaBundleSourceType,
805
        ]);
806
        if (!Craft::$app instanceof ConsoleApplication) {
807
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
808
        }
809
    }
810
811
    /**
812
     * Invalidate a meta bundle cache
813
     *
814
     * @param string $uri
815
     * @param null|int $siteId
816
     */
817
    public function invalidateContainerCacheByPath(string $uri, $siteId = null)
818
    {
819
        $cache = Craft::$app->getCache();
820
        if ($siteId === null) {
821
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
822
        }
823
        TagDependency::invalidate($cache, self::METACONTAINER_CACHE_TAG . $uri . $siteId);
824
        Craft::info(
825
            'Meta container cache cleared: ' . $uri . ' / ' . $siteId,
826
            __METHOD__
827
        );
828
        // Trigger an event to let other plugins/modules know we've cleared our caches
829
        $event = new InvalidateContainerCachesEvent([
830
            'uri' => $uri,
831
            'siteId' => $siteId,
832
            'sourceId' => null,
833
            'sourceType' => null,
834
        ]);
835
        if (!Craft::$app instanceof ConsoleApplication) {
836
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
837
        }
838
    }
839
840
    // Protected Methods
841
    // =========================================================================
842
843
    /**
844
     * Set the element that matches the $uri
845
     *
846
     * @param string $uri
847
     * @param int|null $siteId
848
     */
849
    protected function setMatchedElement(string $uri, int $siteId = null)
850
    {
851
        if ($siteId === null) {
852
            $siteId = Craft::$app->getSites()->currentSite->id
853
                ?? Craft::$app->getSites()->primarySite->id
854
                ?? 1;
855
        }
856
        $uri = trim($uri, '/');
857
        /** @var Element $element */
858
        $enabledOnly = !Seomatic::$previewingMetaContainers;
859
        $element = Craft::$app->getElements()->getElementByUri($uri, $siteId, $enabledOnly);
860
        if ($element && ($element->uri !== null)) {
861
            Seomatic::setMatchedElement($element);
862
        }
863
    }
864
865
    /**
866
     * Return as key/value pairs any allowed parameters in the request
867
     *
868
     * @return string
869
     */
870
    protected function getAllowedUrlParams(): string
871
    {
872
        $result = '';
873
        $allowedParams = Seomatic::$settings->allowedUrlParams;
874
        if (Craft::$app->getPlugins()->getPlugin(SeoProduct::REQUIRED_PLUGIN_HANDLE)) {
875
            $commerce = CommercePlugin::getInstance();
876
            if ($commerce !== null) {
877
                $allowedParams[] = 'variant';
878
            }
879
        }
880
        // Iterate through the allowed parameters, adding the key/value pair to the $result string as found
881
        $request = Craft::$app->getRequest();
882
        if (!$request->isConsoleRequest) {
883
            foreach ($allowedParams as $allowedParam) {
884
                $value = $request->getParam($allowedParam);
885
                if ($value !== null) {
886
                    $result .= "{$allowedParam}={$value}";
887
                }
888
            }
889
        }
890
891
        return $result;
892
    }
893
894
    /**
895
     * Load the meta containers specific to the matched meta bundle
896
     */
897
    protected function loadContentMetaContainers()
898
    {
899
        Craft::beginProfile('MetaContainers::loadContentMetaContainers', __METHOD__);
900
        $metaBundle = $this->getMatchedMetaBundle();
901
        if ($metaBundle) {
902
            $this->addMetaBundleToContainers($metaBundle);
903
        }
904
        Craft::endProfile('MetaContainers::loadContentMetaContainers', __METHOD__);
905
    }
906
907
    /**
908
     * Load any meta containers in the current element
909
     */
910
    protected function loadFieldMetaContainers()
911
    {
912
        Craft::beginProfile('MetaContainers::loadFieldMetaContainers', __METHOD__);
913
        $element = Seomatic::$matchedElement;
914
        if ($element && $this->includeMatchedElement) {
915
            /** @var Element $element */
916
            $fieldHandles = FieldHelper::fieldsOfTypeFromElement($element, FieldHelper::SEO_SETTINGS_CLASS_KEY, true);
917
            foreach ($fieldHandles as $fieldHandle) {
918
                if (!empty($element->$fieldHandle)) {
919
                    /** @var MetaBundle $metaBundle */
920
                    $metaBundle = $element->$fieldHandle;
921
                    Seomatic::$plugin->metaBundles->pruneFieldMetaBundleSettings($metaBundle, $fieldHandle);
922
923
                    // See which properties have to be overridden, because the parent bundle says so.
924
                    foreach (self::COMPOSITE_SETTING_LOOKUP as $settingName => $rules) {
925
                        if (empty($metaBundle->metaGlobalVars->{$settingName})) {
926
                            $parentBundle = Seomatic::$plugin->metaBundles->getContentMetaBundleForElement($element);
927
928
                            foreach ($rules as $settingPath => $action) {
929
                                list ($container, $property) = explode('.', $settingPath);
930
                                list ($testValue, $sourceSetting) = explode('.', $action);
931
932
                                $bundleProp = $parentBundle->{$container}->{$property} ?? null;
933
                                if ($bundleProp == $testValue) {
934
                                    $metaBundle->metaGlobalVars->{$settingName} = $metaBundle->metaGlobalVars->{$sourceSetting};
935
                                }
936
                            }
937
                        }
938
                    }
939
940
                    // Handle re-creating the `mainEntityOfPage` so that the model injected into the
941
                    // templates has the appropriate attributes
942
                    $generalContainerKey = MetaJsonLdContainer::CONTAINER_TYPE . JsonLdService::GENERAL_HANDLE;
943
                    $generalContainer = $this->metaContainers[$generalContainerKey];
944
                    if (($generalContainer !== null) && !empty($generalContainer->data['mainEntityOfPage'])) {
945
                        /** @var MetaJsonLd $jsonLdModel */
946
                        $jsonLdModel = $generalContainer->data['mainEntityOfPage'];
947
                        $config = $jsonLdModel->getAttributes();
948
                        $schemaType = $metaBundle->metaGlobalVars->mainEntityOfPage ?? $config['type'] ?? null;
949
                        // If the schemaType is '' we should fall back on whatever the mainEntityOfPage already is
950
                        if (empty($schemaType)) {
951
                            $schemaType = null;
952
                        }
953
                        if ($schemaType !== null) {
954
                            $config['key'] = 'mainEntityOfPage';
955
                            $schemaType = MetaValueHelper::parseString($schemaType);
956
                            $generalContainer->data['mainEntityOfPage'] = MetaJsonLd::create($schemaType, $config);
957
                        }
958
                    }
959
                    $this->addMetaBundleToContainers($metaBundle);
960
                }
961
            }
962
        }
963
        Craft::endProfile('MetaContainers::loadFieldMetaContainers', __METHOD__);
964
    }
965
966
    /**
967
     * Generate an md5 hash from an object or array
968
     *
969
     * @param string|array|MetaItem $data
970
     *
971
     * @return string
972
     */
973
    protected function getHash($data): string
974
    {
975
        if (is_object($data)) {
976
            $data = $data->toArray();
977
        }
978
        if (is_array($data)) {
979
            $data = serialize($data);
980
        }
981
982
        return md5($data);
983
    }
984
985
    // Private Methods
986
    // =========================================================================
987
}
988