Passed
Push — develop-v4 ( 01c404...563ce7 )
by Andrew
18:55 queued 08:57
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 2
Bugs 2 Features 0
Metric Value
eloc 26
c 2
b 2
f 0
dl 0
loc 42
ccs 0
cts 30
cp 0
rs 7.6666
cc 10
nc 3
nop 1
crap 110

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
     * Include any script body HTML
169
     *
170
     * @param int $bodyPosition
171
     */
172
    public function includeScriptBodyHtml(int $bodyPosition)
173
    {
174
        Craft::beginProfile('MetaContainers::includeScriptBodyHtml', __METHOD__);
175
        $dependency = $this->containerDependency;
176
        $uniqueKey = $dependency->tags[3] ?? self::GLOBALS_CACHE_KEY;
177
        $uniqueKey .= $bodyPosition;
178
        $scriptData = Craft::$app->getCache()->getOrSet(
179
            self::GLOBALS_CACHE_KEY . $uniqueKey,
180
            function () use ($uniqueKey, $bodyPosition) {
181
                Craft::info(
182
                    self::SCRIPTS_CACHE_KEY . ' cache miss: ' . $uniqueKey,
183
                    __METHOD__
184
                );
185
                $scriptData = [];
186
                $scriptContainers = $this->getContainersOfType(MetaScriptContainer::CONTAINER_TYPE);
187
                foreach ($scriptContainers as $scriptContainer) {
188
                    /** @var MetaScriptContainer $scriptContainer */
189
                    if ($scriptContainer->include) {
190
                        if ($scriptContainer->prepForInclusion()) {
191
                            foreach ($scriptContainer->data as $metaScript) {
192
                                /** @var MetaScript $metaScript */
193
                                if (!empty($metaScript->bodyTemplatePath)
194
                                    && ((int)$metaScript->bodyPosition === $bodyPosition)) {
195
                                    $scriptData[] = $metaScript->renderBodyHtml();
196
                                }
197
                            }
198
                        }
199
                    }
200
                }
201
202
                return $scriptData;
203
            },
204
            Seomatic::$cacheDuration,
205
            $dependency
206
        );
207
        // Output the script HTML
208
        foreach ($scriptData as $script) {
209
            if (is_string($script) && !empty($script)) {
210
                echo $script;
211
            }
212
        }
213
        Craft::endProfile('MetaContainers::includeScriptBodyHtml', __METHOD__);
214
    }
215
216
    /**
217
     * Return the containers of a specific type
218
     *
219
     * @param string $type
220
     *
221
     * @return array
222
     */
223
    public function getContainersOfType(string $type): array
224
    {
225
        $containers = [];
226
        /** @var  $metaContainer MetaContainer */
227
        foreach ($this->metaContainers as $metaContainer) {
228
            if ($metaContainer::CONTAINER_TYPE === $type) {
229
                $containers[] = $metaContainer;
230
            }
231
        }
232
233
        return $containers;
234
    }
235
236
    /**
237
     * Include the meta containers
238
     */
239
    public function includeMetaContainers()
240
    {
241
        Craft::beginProfile('MetaContainers::includeMetaContainers', __METHOD__);
242
        // If this page is paginated, we need to factor that into the cache key
243
        // We also need to re-add the hreflangs
244
        if ($this->paginationPage !== '1') {
245
            if (Seomatic::$settings->addHrefLang && Seomatic::$settings->addPaginatedHreflang) {
246
                DynamicMetaHelper::addMetaLinkHrefLang();
247
            }
248
        }
249
        // Add in our http headers
250
        DynamicMetaHelper::includeHttpHeaders();
251
        DynamicMetaHelper::addCspTags();
252
        $this->parseGlobalVars();
253
        foreach ($this->metaContainers as $metaContainer) {
254
            /** @var $metaContainer MetaContainer */
255
            if ($metaContainer->include) {
256
                // Don't cache the rendered result if we're previewing meta containers
257
                if (Seomatic::$previewingMetaContainers) {
258
                    $metaContainer->clearCache = true;
259
                }
260
                $metaContainer->includeMetaData($this->containerDependency);
261
            }
262
        }
263
        Craft::endProfile('MetaContainers::includeMetaContainers', __METHOD__);
264
    }
265
266
    /**
267
     * Parse the global variables
268
     */
269
    public function parseGlobalVars()
270
    {
271
        $dependency = $this->containerDependency;
272
        $uniqueKey = $dependency->tags[3] ?? self::GLOBALS_CACHE_KEY;
273
        list($this->metaGlobalVars, $this->metaSiteVars) = Craft::$app->getCache()->getOrSet(
274
            self::GLOBALS_CACHE_KEY . $uniqueKey,
275
            function () use ($uniqueKey) {
276
                Craft::info(
277
                    self::GLOBALS_CACHE_KEY . ' cache miss: ' . $uniqueKey,
278
                    __METHOD__
279
                );
280
281
                if ($this->metaGlobalVars) {
282
                    $this->metaGlobalVars->parseProperties();
283
                }
284
                if ($this->metaSiteVars) {
285
                    $this->metaSiteVars->parseProperties();
286
                }
287
288
                return [$this->metaGlobalVars, $this->metaSiteVars];
289
            },
290
            Seomatic::$cacheDuration,
291
            $dependency
292
        );
293
    }
294
295
    /**
296
     * Prep all of the meta for preview purposes
297
     *
298
     * @param string $uri
299
     * @param int|null $siteId
300
     * @param bool $parseVariables Whether or not the variables should be
301
     *                                 parsed as Twig
302
     * @param bool $includeElement Whether or not the matched element
303
     *                                 should be factored into the preview
304
     */
305
    public function previewMetaContainers(
306
        string $uri = '',
307
        int    $siteId = null,
308
        bool   $parseVariables = false,
309
        bool   $includeElement = true
310
    )
311
    {
312
        // If we've already previewed the containers for this request, there's no need to do it again
313
        if (Seomatic::$previewingMetaContainers && !Seomatic::$headlessRequest) {
314
            return;
315
        }
316
        // It's possible this won't exist at this point
317
        if (!Seomatic::$seomaticVariable) {
318
            // Create our variable and stash it in the plugin for global access
319
            Seomatic::$seomaticVariable = new SeomaticVariable();
320
        }
321
        Seomatic::$previewingMetaContainers = true;
322
        $this->includeMatchedElement = $includeElement;
323
        $this->loadMetaContainers($uri, $siteId);
324
        // Load in the right globals
325
        $twig = Craft::$app->getView()->getTwig();
0 ignored issues
show
Unused Code introduced by
The assignment to $twig is dead and can be removed.
Loading history...
326
        $globalSets = GlobalSet::findAll([
327
            'siteId' => $siteId,
328
        ]);
329
        foreach ($globalSets as $globalSet) {
330
            MetaValueHelper::$templatePreviewVars[$globalSet->handle] = $globalSet;
331
        }
332
        // Parse the global vars
333
        if ($parseVariables) {
334
            $this->parseGlobalVars();
335
        }
336
        // Get the homeUrl and canonicalUrl
337
        $homeUrl = '/';
338
        $canonicalUrl = $this->metaGlobalVars->parsedValue('canonicalUrl');
339
        $canonicalUrl = DynamicMetaHelper::sanitizeUrl($canonicalUrl, false);
340
        // Special-case the global bundle
341
        if ($uri === MetaBundles::GLOBAL_META_BUNDLE || $uri === '__home__') {
342
            $canonicalUrl = '/';
343
        }
344
        try {
345
            $homeUrl = UrlHelper::siteUrl($homeUrl, null, null, $siteId);
346
            $canonicalUrl = UrlHelper::siteUrl($canonicalUrl, null, null, $siteId);
347
        } catch (Exception $e) {
348
            Craft::error($e->getMessage(), __METHOD__);
349
        }
350
        $canonical = Seomatic::$seomaticVariable->link->get('canonical');
351
        if ($canonical !== null) {
352
            $canonical->href = $canonicalUrl;
353
        }
354
        $home = Seomatic::$seomaticVariable->link->get('home');
355
        if ($home !== null) {
356
            $home->href = $homeUrl;
357
        }
358
        // The current language may _not_ match the current site, if we're headless
359
        $ogLocale = Seomatic::$seomaticVariable->tag->get('og:locale');
360
        if ($ogLocale !== null && $siteId !== null) {
361
            $site = Craft::$app->getSites()->getSiteById($siteId);
362
            if ($site !== null) {
363
                $ogLocale->content = LocalizationHelper::normalizeOgLocaleLanguage($site->language);
364
            }
365
        }
366
        // Update seomatic.meta.canonicalUrl when previewing meta containers
367
        $this->metaGlobalVars->canonicalUrl = $canonicalUrl;
368
    }
369
370
    /**
371
     * Load the meta containers
372
     *
373
     * @param string|null $uri
374
     * @param int|null $siteId
375
     */
376
    public function loadMetaContainers(string $uri = '', int $siteId = null)
377
    {
378
        Craft::beginProfile('MetaContainers::loadMetaContainers', __METHOD__);
379
        // Avoid recursion
380
        if (!Seomatic::$loadingMetaContainers) {
381
            Seomatic::$loadingMetaContainers = true;
382
            $this->setMatchedElement($uri, $siteId);
383
            // Get the cache tag for the matched meta bundle
384
            $metaBundle = $this->getMatchedMetaBundle();
385
            $metaBundleSourceId = '';
386
            $metaBundleSourceType = '';
387
            if ($metaBundle) {
388
                $metaBundleSourceId = $metaBundle->sourceId;
389
                $metaBundleSourceType = $metaBundle->sourceBundleType;
390
            }
391
            // We need an actual $siteId here for the cache key
392
            if ($siteId === null) {
393
                $siteId = Craft::$app->getSites()->currentSite->id
394
                    ?? Craft::$app->getSites()->primarySite->id
395
                    ?? 1;
396
            }
397
            // Handle pagination
398
            $paginationPage = 'page' . $this->paginationPage;
399
            // Get the path for the current request
400
            $request = Craft::$app->getRequest();
401
            $requestPath = '/';
402
            if (!$request->getIsConsoleRequest()) {
403
                try {
404
                    $requestPath = $request->getPathInfo();
405
                } catch (InvalidConfigException $e) {
406
                    Craft::error($e->getMessage(), __METHOD__);
407
                }
408
                // If this is any type of a preview, ensure that it's not cached
409
                if (Seomatic::$plugin->helper::isPreview()) {
410
                    Seomatic::$previewingMetaContainers = true;
411
                }
412
            }
413
            // Get our cache key
414
            $cacheKey = $uri . $siteId . $paginationPage . $requestPath . $this->getAllowedUrlParams();
415
            // For requests with a status code of >= 400, use one cache key
416
            if (!$request->isConsoleRequest) {
417
                $response = Craft::$app->getResponse();
418
                if ($response->statusCode >= 400) {
419
                    $cacheKey = $siteId . self::INVALID_RESPONSE_CACHE_KEY . $response->statusCode;
420
                }
421
            }
422
            // Load the meta containers
423
            $dependency = new TagDependency([
424
                'tags' => [
425
                    self::GLOBAL_METACONTAINER_CACHE_TAG,
426
                    self::METACONTAINER_CACHE_TAG . $metaBundleSourceId . $metaBundleSourceType . $siteId,
427
                    self::METACONTAINER_CACHE_TAG . $uri . $siteId,
428
                    self::METACONTAINER_CACHE_TAG . $cacheKey,
429
                ],
430
            ]);
431
            $this->containerDependency = $dependency;
432
            if (Seomatic::$previewingMetaContainers) {
433
                Seomatic::$plugin->frontendTemplates->loadFrontendTemplateContainers($siteId);
434
                $this->loadGlobalMetaContainers($siteId);
435
                $this->loadContentMetaContainers();
436
                $this->loadFieldMetaContainers();
437
                // We only need the dynamic data for headless requests
438
                if (Seomatic::$headlessRequest || Seomatic::$plugin->helper::isPreview()) {
439
                    DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
440
                }
441
            } else {
442
                $cache = Craft::$app->getCache();
443
                list($this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers) = $cache->getOrSet(
444
                    self::CACHE_KEY . $cacheKey,
445
                    function () use ($uri, $siteId) {
446
                        Craft::info(
447
                            'Meta container cache miss: ' . $uri . '/' . $siteId,
448
                            __METHOD__
449
                        );
450
                        $this->loadGlobalMetaContainers($siteId);
451
                        $this->loadContentMetaContainers();
452
                        $this->loadFieldMetaContainers();
453
                        DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
454
455
                        return [$this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers];
456
                    },
457
                    Seomatic::$cacheDuration,
458
                    $dependency
459
                );
460
            }
461
            Seomatic::$seomaticVariable->init();
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

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