Passed
Push — v3 ( 0924cc...a9bf7a )
by Andrew
34:45 queued 22:10
created

MetaContainers::renderContainersArrayByType()   B

Complexity

Conditions 8
Paths 9

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 8
eloc 16
c 2
b 0
f 0
nc 9
nop 1
dl 0
loc 31
ccs 0
cts 18
cp 0
crap 72
rs 8.4444
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\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...
15
use nystudio107\seomatic\helpers\ArrayHelper;
16
use nystudio107\seomatic\helpers\Json;
17
use nystudio107\seomatic\helpers\Localization as LocalizationHelper;
18
use nystudio107\seomatic\models\FrontendTemplateContainer;
19
use nystudio107\seomatic\models\MetaJsonLd;
20
use nystudio107\seomatic\Seomatic;
21
use nystudio107\seomatic\base\MetaContainer;
22
use nystudio107\seomatic\base\MetaItem;
23
use nystudio107\seomatic\events\InvalidateContainerCachesEvent;
24
use nystudio107\seomatic\helpers\DynamicMeta as DynamicMetaHelper;
25
use nystudio107\seomatic\helpers\Field as FieldHelper;
26
use nystudio107\seomatic\helpers\MetaValue as MetaValueHelper;
27
use nystudio107\seomatic\helpers\UrlHelper;
28
use nystudio107\seomatic\models\MetaBundle;
29
use nystudio107\seomatic\models\MetaGlobalVars;
30
use nystudio107\seomatic\models\MetaSiteVars;
31
use nystudio107\seomatic\models\MetaSitemapVars;
32
use nystudio107\seomatic\models\MetaJsonLdContainer;
33
use nystudio107\seomatic\models\MetaLinkContainer;
34
use nystudio107\seomatic\models\MetaScriptContainer;
35
use nystudio107\seomatic\models\MetaScript;
36
use nystudio107\seomatic\models\MetaTagContainer;
37
use nystudio107\seomatic\models\MetaTitleContainer;
38
use nystudio107\seomatic\services\JsonLd as JsonLdService;
39
use nystudio107\seomatic\variables\SeomaticVariable;
40
41
use Craft;
42
use craft\base\Component;
43
use craft\base\Element;
44
use craft\console\Application as ConsoleApplication;
45
use craft\elements\GlobalSet;
46
47
use yii\base\Exception;
48
use yii\base\InvalidConfigException;
49
use yii\caching\TagDependency;
50
51
/**
52
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
53
 * @package   Seomatic
54
 * @since     3.0.0
55
 */
56
class MetaContainers extends Component
57
{
58
    // Constants
59
    // =========================================================================
60
61
    const GLOBAL_METACONTAINER_CACHE_TAG = 'seomatic_metacontainer';
62
    const METACONTAINER_CACHE_TAG = 'seomatic_metacontainer_';
63
64
    const CACHE_KEY = 'seomatic_metacontainer_';
65
    const INVALID_RESPONSE_CACHE_KEY = 'seomatic_invalid_response';
66
    const GLOBALS_CACHE_KEY = 'parsed_globals_';
67
    const SCRIPTS_CACHE_KEY = 'body_scripts_';
68
69
    /** @var array Rules for replacement values on arbitrary empty values */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
70
    const COMPOSITE_SETTING_LOOKUP = [
71
        'ogImage' => [
72
            'metaBundleSettings.ogImageSource' => 'sameAsSeo.seoImage',
73
        ],
74
        'twitterImage' => [
75
            'metaBundleSettings.twitterImageSource' => 'sameAsSeo.seoImage',
76
        ],
77
    ];
78
79
    /**
80
     * @event InvalidateContainerCachesEvent The event that is triggered when SEOmatic
81
     *        is about to clear its meta container caches
82
     *
83
     * ---
84
     * ```php
85
     * use nystudio107\seomatic\events\InvalidateContainerCachesEvent;
86
     * use nystudio107\seomatic\services\MetaContainers;
87
     * use yii\base\Event;
88
     * Event::on(MetaContainers::class, MetaContainers::EVENT_INVALIDATE_CONTAINER_CACHES, function(InvalidateContainerCachesEvent $e) {
89
     *     // Container caches are about to be cleared
90
     * });
91
     * ```
92
     */
93
    const EVENT_INVALIDATE_CONTAINER_CACHES = 'invalidateContainerCaches';
94
95
    // Public Properties
96
    // =========================================================================
97
98
    /**
99
     * @var MetaGlobalVars
100
     */
101
    public $metaGlobalVars;
102
103
    /**
104
     * @var MetaSiteVars
105
     */
106
    public $metaSiteVars;
107
108
    /**
109
     * @var MetaSitemapVars
110
     */
111
    public $metaSitemapVars;
112
113
    /**
114
     * @var string The current page number of paginated pages
115
     */
116
    public $paginationPage = '1';
117
118
    /**
119
     * @var null|string Cached nonce to be shared by all JSON-LD entities
120
     */
121
    public $cachedJsonLdNonce;
122
123
    // Protected Properties
124
    // =========================================================================
125
126
    /**
127
     * @var MetaContainer
128
     */
129
    protected $metaContainers = [];
130
131
    /**
132
     * @var null|MetaBundle
133
     */
134
    protected $matchedMetaBundle;
135
136
    /**
137
     * @var null|TagDependency
138
     */
139
    protected $containerDependency;
140
141
    /**
142
     * @var bool Whether or not the matched element should be included in the
143
     *      meta containers
144
     */
145
    protected $includeMatchedElement = true;
146
147
    // Public Methods
148
    // =========================================================================
149
150
    /**
151
     * @inheritdoc
152
     */
153 1
    public function init()
154
    {
155 1
        parent::init();
156
        // Get the page number of this request
157 1
        $request = Craft::$app->getRequest();
158 1
        if (!$request->isConsoleRequest) {
159
            $this->paginationPage = (string)$request->pageNum;
160
        }
161 1
    }
162
163
    /**
164
     * Load the meta containers
165
     *
166
     * @param string|null $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
167
     * @param int|null    $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
168
     */
169
    public function loadMetaContainers(string $uri = '', int $siteId = null)
170
    {
171
        Craft::beginProfile('MetaContainers::loadMetaContainers', __METHOD__);
172
        // Avoid recursion
173
        if (!Seomatic::$loadingMetaContainers) {
174
            Seomatic::$loadingMetaContainers = true;
175
            $this->setMatchedElement($uri, $siteId);
176
            // Get the cache tag for the matched meta bundle
177
            $metaBundle = $this->getMatchedMetaBundle();
178
            $metaBundleSourceId = '';
179
            $metaBundleSourceType = '';
180
            if ($metaBundle) {
181
                $metaBundleSourceId = $metaBundle->sourceId;
182
                $metaBundleSourceType = $metaBundle->sourceBundleType;
183
            }
184
            // We need an actual $siteId here for the cache key
185
            if ($siteId === null) {
186
                $siteId = Craft::$app->getSites()->currentSite->id
187
                    ?? Craft::$app->getSites()->primarySite->id
188
                    ?? 1;
189
            }
190
            // Handle pagination
191
            $paginationPage = 'page'.$this->paginationPage;
192
            // Get the path for the current request
193
            $request = Craft::$app->getRequest();
194
            $requestPath = '/';
195
            if (!$request->getIsConsoleRequest()) {
196
                try {
197
                    $requestPath = $request->getPathInfo();
198
                } catch (InvalidConfigException $e) {
199
                    Craft::error($e->getMessage(), __METHOD__);
200
                }
201
                // If this is any type of a preview, ensure that it's not cached
202
                if (Seomatic::$plugin->helper::isPreview()) {
203
                    Seomatic::$previewingMetaContainers = true;
204
                }
205
            }
206
            // Get our cache key
207
            $cacheKey = $uri.$siteId.$paginationPage.$requestPath.$this->getAllowedUrlParams();
208
            // For requests with a status code of >= 400, use one cache key
209
            if (!$request->isConsoleRequest) {
210
                $response = Craft::$app->getResponse();
211
                if ($response->statusCode >= 400) {
212
                    $cacheKey = $siteId.self::INVALID_RESPONSE_CACHE_KEY.$response->statusCode;
213
                }
214
            }
215
            // Load the meta containers
216
            $dependency = new TagDependency([
217
                'tags' => [
218
                    self::GLOBAL_METACONTAINER_CACHE_TAG,
219
                    self::METACONTAINER_CACHE_TAG.$metaBundleSourceId.$metaBundleSourceType.$siteId,
220
                    self::METACONTAINER_CACHE_TAG.$uri.$siteId,
221
                    self::METACONTAINER_CACHE_TAG.$cacheKey,
222
                ],
223
            ]);
224
            $this->containerDependency = $dependency;
225
            if (Seomatic::$previewingMetaContainers) {
226
                $this->loadGlobalMetaContainers($siteId);
227
                $this->loadContentMetaContainers();
228
                $this->loadFieldMetaContainers();
229
                // We only need the dynamic data for headless requests
230
                if (Seomatic::$headlessRequest || Seomatic::$plugin->helper::isPreview()) {
231
                    DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
232
                }
233
            } else {
234
                $cache = Craft::$app->getCache();
235
                list($this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers) = $cache->getOrSet(
236
                    self::CACHE_KEY.$cacheKey,
237
                    function () use ($uri, $siteId) {
238
                        Craft::info(
239
                            'Meta container cache miss: '.$uri.'/'.$siteId,
240
                            __METHOD__
241
                        );
242
                        $this->loadGlobalMetaContainers($siteId);
243
                        $this->loadContentMetaContainers();
244
                        $this->loadFieldMetaContainers();
245
                        DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
246
247
                        return [$this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers];
248
                    },
249
                    Seomatic::$cacheDuration,
250
                    $dependency
251
                );
252
            }
253
            Seomatic::$seomaticVariable->init();
254
            MetaValueHelper::cache();
255
            Seomatic::$loadingMetaContainers = false;
256
        }
257
        Craft::endProfile('MetaContainers::loadMetaContainers', __METHOD__);
258
    }
259
260
    /**
261
     * Include any script body HTML
262
     *
263
     * @param int $bodyPosition
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
264
     */
265
    public function includeScriptBodyHtml(int $bodyPosition)
266
    {
267
        Craft::beginProfile('MetaContainers::includeScriptBodyHtml', __METHOD__);
268
        $dependency = $this->containerDependency;
269
        $uniqueKey = $dependency->tags[3] ?? self::GLOBALS_CACHE_KEY;
270
        $uniqueKey .= $bodyPosition;
271
        $scriptData = Craft::$app->getCache()->getOrSet(
272
            self::GLOBALS_CACHE_KEY.$uniqueKey,
273
            function () use ($uniqueKey, $bodyPosition) {
274
                Craft::info(
275
                    self::SCRIPTS_CACHE_KEY.' cache miss: '.$uniqueKey,
276
                    __METHOD__
277
                );
278
                $scriptData = [];
279
                $scriptContainers = $this->getContainersOfType(MetaScriptContainer::CONTAINER_TYPE);
280
                foreach ($scriptContainers as $scriptContainer) {
281
                    /** @var MetaScriptContainer $scriptContainer */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
282
                    if ($scriptContainer->include) {
283
                        if ($scriptContainer->prepForInclusion()) {
284
                            foreach ($scriptContainer->data as $metaScript) {
285
                                /** @var MetaScript $metaScript */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
286
                                if (!empty($metaScript->bodyTemplatePath)
287
                                    && ((int)$metaScript->bodyPosition === $bodyPosition)) {
0 ignored issues
show
Coding Style introduced by
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
288
                                    $scriptData[] = $metaScript->renderBodyHtml();
289
                                }
290
                            }
291
                        }
292
                    }
293
                }
294
295
                return $scriptData;
296
            },
297
            Seomatic::$cacheDuration,
298
            $dependency
299
        );
300
        // Output the script HTML
301
        foreach ($scriptData as $script) {
302
            if (is_string($script) && !empty($script)) {
303
                echo $script;
304
            }
305
        }
306
        Craft::endProfile('MetaContainers::includeScriptBodyHtml', __METHOD__);
307
    }
308
309
    /**
310
     * Include the meta containers
311
     */
312
    public function includeMetaContainers()
313
    {
314
        Craft::beginProfile('MetaContainers::includeMetaContainers', __METHOD__);
315
        // If this page is paginated, we need to factor that into the cache key
316
        // We also need to re-add the hreflangs
317
        if ($this->paginationPage !== '1') {
318
            if (Seomatic::$settings->addHrefLang && Seomatic::$settings->addPaginatedHreflang) {
319
                DynamicMetaHelper::addMetaLinkHrefLang();
320
            }
321
        }
322
        // Add in our http headers
323
        DynamicMetaHelper::includeHttpHeaders();
324
        DynamicMetaHelper::addCspTags();
325
        $this->parseGlobalVars();
326
        foreach ($this->metaContainers as $metaContainer) {
327
            /** @var $metaContainer MetaContainer */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
328
            if ($metaContainer->include) {
329
                // Don't cache the rendered result if we're previewing meta containers
330
                if (Seomatic::$previewingMetaContainers) {
331
                    $metaContainer->clearCache = true;
332
                }
333
                $metaContainer->includeMetaData($this->containerDependency);
334
            }
335
        }
336
        Craft::endProfile('MetaContainers::includeMetaContainers', __METHOD__);
337
    }
338
339
    /**
340
     * Parse the global variables
341
     */
342
    public function parseGlobalVars()
343
    {
344
        $dependency = $this->containerDependency;
345
        $uniqueKey = $dependency->tags[3] ?? self::GLOBALS_CACHE_KEY;
346
        list($this->metaGlobalVars, $this->metaSiteVars) = Craft::$app->getCache()->getOrSet(
347
            self::GLOBALS_CACHE_KEY.$uniqueKey,
348
            function () use ($uniqueKey) {
349
                Craft::info(
350
                    self::GLOBALS_CACHE_KEY.' cache miss: '.$uniqueKey,
351
                    __METHOD__
352
                );
353
354
                if ($this->metaGlobalVars) {
355
                    $this->metaGlobalVars->parseProperties();
356
                }
357
                if ($this->metaSiteVars) {
358
                    $this->metaSiteVars->parseProperties();
359
                }
360
361
                return [$this->metaGlobalVars, $this->metaSiteVars];
362
            },
363
            Seomatic::$cacheDuration,
364
            $dependency
365
        );
366
    }
367
368
    /**
369
     * Prep all of the meta for preview purposes
370
     *
371
     * @param string   $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
372
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
373
     * @param bool     $parseVariables Whether or not the variables should be
374
     *                                 parsed as Twig
375
     * @param bool     $includeElement Whether or not the matched element
376
     *                                 should be factored into the preview
377
     */
378
    public function previewMetaContainers(
379
        string $uri = '',
380
        int $siteId = null,
381
        bool $parseVariables = false,
382
        bool $includeElement = true
383
    ) {
384
        // If we've already previewed the containers for this request, there's no need to do it again
385
        if (Seomatic::$previewingMetaContainers && !Seomatic::$headlessRequest) {
386
            return;
387
        }
388
        // It's possible this won't exist at this point
389
        if (!Seomatic::$seomaticVariable) {
390
            // Create our variable and stash it in the plugin for global access
391
            Seomatic::$seomaticVariable = new SeomaticVariable();
392
        }
393
        Seomatic::$previewingMetaContainers = true;
394
        $this->includeMatchedElement = $includeElement;
395
        $this->loadMetaContainers($uri, $siteId);
396
        // Load in the right globals
397
        $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...
398
        $globalSets = GlobalSet::findAll([
399
            'siteId' => $siteId,
400
        ]);
401
        MetaValueHelper::$templatePreviewVars = [];
402
        foreach ($globalSets as $globalSet) {
403
            MetaValueHelper::$templatePreviewVars[$globalSet->handle] = $globalSet;
404
        }
405
        // Parse the global vars
406
        if ($parseVariables) {
407
            $this->parseGlobalVars();
408
        }
409
        // Get the homeUrl and canonicalUrl
410
        $homeUrl = '/';
411
        $canonicalUrl = DynamicMetaHelper::sanitizeUrl($uri, false);
412
        // Special-case the global bundle
413
        if ($uri === MetaBundles::GLOBAL_META_BUNDLE || $uri === '__home__') {
414
            $canonicalUrl = '/';
415
        }
416
        try {
417
            $homeUrl = UrlHelper::siteUrl($homeUrl, null, null, $siteId);
418
            $canonicalUrl = UrlHelper::siteUrl($canonicalUrl, null, null, $siteId);
419
        } catch (Exception $e) {
420
            Craft::error($e->getMessage(), __METHOD__);
421
        }
422
        $canonical = Seomatic::$seomaticVariable->link->get('canonical');
423
        if ($canonical !== null) {
424
            $canonical->href = $canonicalUrl;
425
        }
426
        $home = Seomatic::$seomaticVariable->link->get('home');
427
        if ($home !== null) {
428
            $home->href = $homeUrl;
429
        }
430
        // The current language may _not_ match the current site, if we're headless
431
        $ogLocale = Seomatic::$seomaticVariable->tag->get('og:locale');
432
        if ($ogLocale !== null && $siteId !== null) {
433
            $site = Craft::$app->getSites()->getSiteById($siteId);
434
            if ($site !== null) {
435
                $ogLocale->content = LocalizationHelper::normalizeOgLocaleLanguage($site->language);
436
            }
437
        }
438
        // Update seomatic.meta.canonicalUrl when previewing meta containers
439
        $this->metaGlobalVars->canonicalUrl = $canonicalUrl;
440
    }
441
442
    /**
443
     * Add the passed in MetaItem to the MetaContainer indexed as $key
444
     *
445
     * @param $data MetaItem The MetaItem to add to the container
446
     * @param $key  string   The key to the container to add the data to
447
     */
448 1
    public function addToMetaContainer(MetaItem $data, string $key)
449
    {
450
        /** @var  $container MetaContainer */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
451 1
        $container = $this->getMetaContainer($key);
452
453 1
        if ($container !== null) {
454
            $container->addData($data, $data->key);
455
        }
456 1
    }
457
458
    /**
459
     * @param string $key
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
460
     *
461
     * @return mixed|null
462
     */
463 1
    public function getMetaContainer(string $key)
464
    {
465 1
        if (!$key || empty($this->metaContainers[$key])) {
466 1
            $error = Craft::t(
467 1
                'seomatic',
468 1
                'Meta container with key `{key}` does not exist.',
469 1
                ['key' => $key]
470
            );
471 1
            Craft::error($error, __METHOD__);
472
473 1
            return null;
474
        }
475
476
        return $this->metaContainers[$key];
477
    }
478
479
    /**
480
     * Create a MetaContainer of the given $type with the $key
481
     *
482
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
483
     * @param string $key
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
484
     *
485
     * @return null|MetaContainer
486
     */
487
    public function createMetaContainer(string $type, string $key): MetaContainer
488
    {
489
        /** @var MetaContainer $container */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
490
        $container = null;
491
        if (empty($this->metaContainers[$key])) {
492
            /** @var MetaContainer $className */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
493
            $className = null;
494
            // Create a new container based on the type passed in
495
            switch ($type) {
496
                case MetaTagContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
497
                    $className = MetaTagContainer::class;
498
                    break;
499
                case MetaLinkContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
500
                    $className = MetaLinkContainer::class;
501
                    break;
502
                case MetaScriptContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
503
                    $className = MetaScriptContainer::class;
504
                    break;
505
                case MetaJsonLdContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
506
                    $className = MetaJsonLdContainer::class;
507
                    break;
508
                case MetaTitleContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
509
                    $className = MetaTitleContainer::class;
510
                    break;
511
            }
512
            if ($className) {
513
                $container = $className::create();
514
                if ($container) {
0 ignored issues
show
introduced by
$container is of type nystudio107\seomatic\base\Container, thus it always evaluated to true.
Loading history...
515
                    $this->metaContainers[$key] = $container;
516
                }
517
            }
518
        }
519
520
        /** @var MetaContainer $className */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
521
        return $container;
522
    }
523
524
    /**
525
     * Return the containers of a specific type
526
     *
527
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
528
     *
529
     * @return array
530
     */
531
    public function getContainersOfType(string $type): array
532
    {
533
        $containers = [];
534
        /** @var  $metaContainer MetaContainer */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
535
        foreach ($this->metaContainers as $metaContainer) {
536
            if ($metaContainer::CONTAINER_TYPE === $type) {
537
                $containers[] = $metaContainer;
538
            }
539
        }
540
541
        return $containers;
542
    }
543
544
    /**
545
     * Render the HTML of all MetaContainers of a specific $type
546
     *
547
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
548
     *
549
     * @return string
550
     */
551
    public function renderContainersByType(string $type): string
552
    {
553
        $html = '';
554
        // Special-case for requests for the FrontendTemplateContainer "container"
555
        if ($type === FrontendTemplateContainer::CONTAINER_TYPE) {
556
            $renderedTemplates = [];
557
            $frontendTemplateContainers = Seomatic::$plugin->frontendTemplates->frontendTemplateContainer['data'];
558
            foreach ($frontendTemplateContainers as $name => $frontendTemplateContainer) {
559
                if ($frontendTemplateContainer->include) {
560
                    $result = $frontendTemplateContainer->render([
561
                    ]);
562
                    $renderedTemplates[] = [$name => $result];
563
                }
564
            }
565
            $html .= Json::encode($renderedTemplates);
566
567
            return $html;
568
        }
569
        /** @var  $metaContainer MetaContainer */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
570
        foreach ($this->metaContainers as $metaContainer) {
571
            if ($metaContainer::CONTAINER_TYPE === $type && $metaContainer->include) {
572
                $result = $metaContainer->render([
573
                    'renderRaw'        => true,
574
                    'renderScriptTags' => true,
575
                    'array'            => true,
576
                ]);
577
                // Special case for script containers, because they can have body scripts too
578
                if ($metaContainer::CONTAINER_TYPE === MetaScriptContainer::CONTAINER_TYPE) {
579
                    $bodyScript = '';
580
                    /** @var MetaScriptContainer $metaContainer */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
581
                    if ($metaContainer->prepForInclusion()) {
582
                        foreach ($metaContainer->data as $metaScript) {
583
                            /** @var MetaScript $metaScript */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
584
                            if (!empty($metaScript->bodyTemplatePath)) {
585
                                $bodyScript .= $metaScript->renderBodyHtml();
586
                            }
587
                        }
588
                    }
589
590
                    $result = Json::encode([
591
                        'script' => $result,
592
                        'bodyScript' => $bodyScript,
593
                    ]);
594
                }
595
596
                $html .= $result;
597
            }
598
        }
599
        // Special-case for requests for the MetaSiteVars "container"
600
        if ($type === MetaSiteVars::CONTAINER_TYPE) {
601
            $result = Json::encode($this->metaSiteVars->toArray());
602
            $html .= $result;
603
        }
604
605
        return $html;
606
    }
607
608
    /**
609
     * Render the HTML of all MetaContainers of a specific $type as an array
610
     *
611
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
612
     *
613
     * @return array
614
     */
615
    public function renderContainersArrayByType(string $type): array
616
    {
617
        $htmlArray = [];
618
        // Special-case for requests for the FrontendTemplateContainer "container"
619
        if ($type === FrontendTemplateContainer::CONTAINER_TYPE) {
620
            $renderedTemplates = [];
621
            $frontendTemplateContainers = Seomatic::$plugin->frontendTemplates->frontendTemplateContainer['data'];
622
            foreach ($frontendTemplateContainers as $name => $frontendTemplateContainer) {
623
                if ($frontendTemplateContainer->include) {
624
                    $result = $frontendTemplateContainer->render([
625
                    ]);
626
                    $renderedTemplates[] = [$name => $result];
627
                }
628
            }
629
630
            return $renderedTemplates;
631
        }
632
        /** @var  $metaContainer MetaContainer */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
633
        foreach ($this->metaContainers as $metaContainer) {
634
            if ($metaContainer::CONTAINER_TYPE === $type && $metaContainer->include) {
635
                /** @noinspection SlowArrayOperationsInLoopInspection */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
636
                $htmlArray = array_merge($htmlArray, $metaContainer->renderArray());
637
            }
638
        }
639
        // Special-case for requests for the MetaSiteVars "container"
640
        if ($type === MetaSiteVars::CONTAINER_TYPE) {
641
            $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...
642
            $htmlArray = array_merge($htmlArray, $this->metaSiteVars->toArray());
643
        }
644
645
        return $htmlArray;
646
    }
647
648
    /**
649
     * Return a MetaItem object by $key from container $type
650
     *
651
     * @param string $key
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
652
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
653
     *
654
     * @return null|MetaItem
655
     */
656
    public function getMetaItemByKey(string $key, string $type = '')
657
    {
658
        $metaItem = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $metaItem is dead and can be removed.
Loading history...
659
        /** @var  $metaContainer MetaContainer */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
660
        foreach ($this->metaContainers as $metaContainer) {
661
            if (($metaContainer::CONTAINER_TYPE === $type) || empty($type)) {
662
                /** @var  $metaTag MetaItem */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
663
                foreach ($metaContainer->data as $metaItem) {
664
                    if ($key === $metaItem->key) {
665
                        return $metaItem;
666
                    }
667
                }
668
            }
669
        }
670
671
        return null;
672
    }
673
674
    // Protected Methods
675
    // =========================================================================
676
677
    /**
678
     * Return the MetaBundle that corresponds with the Seomatic::$matchedElement
679
     *
680
     * @return null|MetaBundle
681
     */
682
    public function getMatchedMetaBundle()
683
    {
684
        $metaBundle = null;
685
        /** @var Element $element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
686
        $element = Seomatic::$matchedElement;
687
        if ($element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
688
            $sourceType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element);
689
            if ($sourceType) {
690
                list($sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId, $typeId)
691
                    = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($element);
692
                $metaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
693
                    $sourceType,
694
                    $sourceId,
695
                    $sourceSiteId,
696
                    $typeId
697
                );
698
            }
699
        }
700
        $this->matchedMetaBundle = $metaBundle;
701
702
        return $metaBundle;
703
    }
704
705
    /**
706
     * Add the meta bundle to our existing meta containers, overwriting meta
707
     * items with the same key
708
     *
709
     * @param MetaBundle $metaBundle
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
710
     */
711
    public function addMetaBundleToContainers(MetaBundle $metaBundle)
712
    {
713
        // Ensure the variable is synced properly first
714
        Seomatic::$seomaticVariable->init();
715
        // Meta global vars
716
        $attributes = $metaBundle->metaGlobalVars->getAttributes();
717
        // Parse the meta values so we can filter out any blank or empty attributes
718
        // So that they can fall back on the parent container
719
        $parsedAttributes = $attributes;
720
        MetaValueHelper::parseArray($parsedAttributes);
721
        $parsedAttributes = array_filter(
722
            $parsedAttributes,
723
            [ArrayHelper::class, 'preserveBools']
724
        );
725
        $attributes = array_intersect_key($attributes, $parsedAttributes);
726
        // Add the attributes in
727
        $attributes = array_filter(
728
            $attributes,
729
            [ArrayHelper::class, 'preserveBools']
730
        );
731
        $this->metaGlobalVars->setAttributes($attributes, false);
732
        // Meta site vars
733
        /*
734
         * Don't merge in the Site vars, since they are only editable on
735
         * a global basis. Otherwise stale data will be unable to be edited
736
        $attributes = $metaBundle->metaSiteVars->getAttributes();
737
        $attributes = array_filter($attributes);
738
        $this->metaSiteVars->setAttributes($attributes, false);
739
        */
740
        // Meta sitemap vars
741
        $attributes = $metaBundle->metaSitemapVars->getAttributes();
742
        $attributes = array_filter(
743
            $attributes,
744
            [ArrayHelper::class, 'preserveBools']
745
        );
746
        $this->metaSitemapVars->setAttributes($attributes, false);
747
        // Language
748
        $this->metaGlobalVars->language = Seomatic::$language;
749
        // Meta containers
750
        foreach ($metaBundle->metaContainers as $key => $metaContainer) {
751
            foreach ($metaContainer->data as $metaTag) {
752
                $this->addToMetaContainer($metaTag, $key);
753
            }
754
        }
755
    }
756
757
    /**
758
     * Invalidate all of the meta container caches
759
     */
760
    public function invalidateCaches()
761
    {
762
        $cache = Craft::$app->getCache();
763
        TagDependency::invalidate($cache, self::GLOBAL_METACONTAINER_CACHE_TAG);
764
        Craft::info(
765
            'All meta container caches cleared',
766
            __METHOD__
767
        );
768
        // Trigger an event to let other plugins/modules know we've cleared our caches
769
        $event = new InvalidateContainerCachesEvent([
770
            'uri' => null,
771
            'siteId' => null,
772
            'sourceId' => null,
773
            'sourceType' => null,
774
        ]);
775
        if (!Craft::$app instanceof ConsoleApplication) {
776
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
777
        }
778
    }
779
780
    /**
781
     * Invalidate a meta bundle cache
782
     *
783
     * @param int          $sourceId
0 ignored issues
show
Coding Style introduced by
Expected 9 spaces after parameter type; 10 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
784
     * @param null|string  $sourceType
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter type; 2 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
785
     * @param null|int     $siteId
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter type; 5 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
786
     */
787
    public function invalidateContainerCacheById(int $sourceId, $sourceType = null, $siteId = null)
788
    {
789
        $metaBundleSourceId = '';
790
        if ($sourceId) {
791
            $metaBundleSourceId = $sourceId;
792
        }
793
        $metaBundleSourceType = '';
794
        if ($sourceType) {
795
            $metaBundleSourceType = $sourceType;
796
        }
797
        if ($siteId === null) {
798
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
799
        }
800
        $cache = Craft::$app->getCache();
801
        TagDependency::invalidate(
802
            $cache,
803
            self::METACONTAINER_CACHE_TAG.$metaBundleSourceId.$metaBundleSourceType.$siteId
804
        );
805
        Craft::info(
806
            'Meta bundle cache cleared: '.$metaBundleSourceId.' / '.$metaBundleSourceType.' / '.$siteId,
807
            __METHOD__
808
        );
809
        // Trigger an event to let other plugins/modules know we've cleared our caches
810
        $event = new InvalidateContainerCachesEvent([
811
            'uri' => null,
812
            'siteId' => $siteId,
813
            'sourceId' => $sourceId,
814
            'sourceType' => $metaBundleSourceType,
815
        ]);
816
        if (!Craft::$app instanceof ConsoleApplication) {
817
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
818
        }
819
    }
820
821
    /**
822
     * Invalidate a meta bundle cache
823
     *
824
     * @param string   $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
825
     * @param null|int $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
826
     */
827
    public function invalidateContainerCacheByPath(string $uri, $siteId = null)
828
    {
829
        $cache = Craft::$app->getCache();
830
        if ($siteId === null) {
831
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
832
        }
833
        TagDependency::invalidate($cache, self::METACONTAINER_CACHE_TAG.$uri.$siteId);
834
        Craft::info(
835
            'Meta container cache cleared: '.$uri.' / '.$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' => $uri,
841
            'siteId' => $siteId,
842
            'sourceId' => null,
843
            'sourceType' => null,
844
        ]);
845
        if (!Craft::$app instanceof ConsoleApplication) {
846
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
847
        }
848
    }
849
850
    /**
851
     * Load the global site meta containers
852
     *
853
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
854
     */
855
    public function loadGlobalMetaContainers(int $siteId = null)
856
    {
857
        Craft::beginProfile('MetaContainers::loadGlobalMetaContainers', __METHOD__);
858
        if ($siteId === null) {
859
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
860
        }
861
        $metaBundle = Seomatic::$plugin->metaBundles->getGlobalMetaBundle($siteId);
862
        if ($metaBundle) {
863
            // Meta global vars
864
            $this->metaGlobalVars = clone $metaBundle->metaGlobalVars;
865
            // Meta site vars
866
            $this->metaSiteVars = clone $metaBundle->metaSiteVars;
867
            // Meta sitemap vars
868
            $this->metaSitemapVars = clone $metaBundle->metaSitemapVars;
869
            // Language
870
            $this->metaGlobalVars->language = Seomatic::$language;
871
            // Meta containers
872
            foreach ($metaBundle->metaContainers as $key => $metaContainer) {
873
                $this->metaContainers[$key] = clone $metaContainer;
874
            }
875
        }
876
        Craft::endProfile('MetaContainers::loadGlobalMetaContainers', __METHOD__);
877
    }
878
879
    // Protected Methods
880
    // =========================================================================
881
882
    /**
883
     * Return as key/value pairs any allowed parameters in the request
884
     *
885
     * @return string
886
     */
887
    protected function getAllowedUrlParams(): string
888
    {
889
        $result = '';
890
        $allowedParams = Seomatic::$settings->allowedUrlParams;
891
        $commerce = CommercePlugin::getInstance();
892
        if ($commerce !== null) {
893
            $allowedParams[] = 'variant';
894
        }
895
        // Iterate through the allowed parameters, adding the key/value pair to the $result string as found
896
        $request = Craft::$app->getRequest();
897
        if (!$request->isConsoleRequest) {
898
            foreach ($allowedParams as $allowedParam) {
899
                $value = $request->getParam($allowedParam);
900
                if ($value !== null) {
901
                    $result .= "{$allowedParam}={$value}";
902
                }
903
            }
904
        }
905
906
        return $result;
907
    }
908
909
    /**
910
     * Load the meta containers specific to the matched meta bundle
911
     */
912
    protected function loadContentMetaContainers()
913
    {
914
        Craft::beginProfile('MetaContainers::loadContentMetaContainers', __METHOD__);
915
        $metaBundle = $this->getMatchedMetaBundle();
916
        if ($metaBundle) {
917
            $this->addMetaBundleToContainers($metaBundle);
918
        }
919
        Craft::endProfile('MetaContainers::loadContentMetaContainers', __METHOD__);
920
    }
921
922
    /**
923
     * Load any meta containers in the current element
924
     */
925
    protected function loadFieldMetaContainers()
926
    {
927
        Craft::beginProfile('MetaContainers::loadFieldMetaContainers', __METHOD__);
928
        $element = Seomatic::$matchedElement;
929
        if ($element && $this->includeMatchedElement) {
930
            /** @var Element $element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
931
            $fieldHandles = FieldHelper::fieldsOfTypeFromElement($element, FieldHelper::SEO_SETTINGS_CLASS_KEY, true);
932
            foreach ($fieldHandles as $fieldHandle) {
933
                if (!empty($element->$fieldHandle)) {
934
                    /** @var MetaBundle $metaBundle */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
935
                    $metaBundle = $element->$fieldHandle;
936
                    Seomatic::$plugin->metaBundles->pruneFieldMetaBundleSettings($metaBundle, $fieldHandle);
937
938
                    // See which properties have to be overridden, because the parent bundle says so.
939
                    foreach (self::COMPOSITE_SETTING_LOOKUP as $settingName => $rules) {
940
                        if (empty($metaBundle->metaGlobalVars->{$settingName})) {
941
                            $parentBundle = Seomatic::$plugin->metaBundles->getContentMetaBundleForElement($element);
942
943
                            foreach ($rules as $settingPath => $action) {
944
                                list ($container, $property) = explode('.', $settingPath);
945
                                list ($testValue, $sourceSetting) = explode('.', $action);
946
947
                                if ($parentBundle->{$container}->{$property} == $testValue) {
948
                                    $metaBundle->metaGlobalVars->{$settingName}  = $metaBundle->metaGlobalVars->{$sourceSetting};
949
                                }
950
                            }
951
                        }
952
                    }
953
954
                    // Handle re-creating the `mainEntityOfPage` so that the model injected into the
955
                    // templates has the appropriate attributes
956
                    $generalContainerKey = MetaJsonLdContainer::CONTAINER_TYPE.JsonLdService::GENERAL_HANDLE;
957
                    $generalContainer = $this->metaContainers[$generalContainerKey];
958
                    if (($generalContainer !== null) && !empty($generalContainer->data['mainEntityOfPage'])) {
959
                        /** @var MetaJsonLd $jsonLdModel */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
960
                        $jsonLdModel = $generalContainer->data['mainEntityOfPage'];
961
                        $config = $jsonLdModel->getAttributes();
962
                        $schemaType = $metaBundle->metaGlobalVars->mainEntityOfPage ?? $config['type'] ?? null;
963
                        // If the schemaType is '' we should fall back on whatever the mainEntityOfPage already is
964
                        if (empty($schemaType)) {
965
                            $schemaType = null;
966
                        }
967
                        if ($schemaType !== null) {
968
                            $config['key'] = 'mainEntityOfPage';
969
                            $schemaType = MetaValueHelper::parseString($schemaType);
970
                            $generalContainer->data['mainEntityOfPage'] = MetaJsonLd::create($schemaType, $config);
971
                        }
972
                    }
973
                    $this->addMetaBundleToContainers($metaBundle);
974
                }
975
            }
976
        }
977
        Craft::endProfile('MetaContainers::loadFieldMetaContainers', __METHOD__);
978
    }
979
980
    /**
981
     * Set the element that matches the $uri
982
     *
983
     * @param string   $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
984
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
985
     */
986
    protected function setMatchedElement(string $uri, int $siteId = null)
987
    {
988
        if ($siteId === null) {
989
            $siteId = Craft::$app->getSites()->currentSite->id
990
                ?? Craft::$app->getSites()->primarySite->id
991
                ?? 1;
992
        }
993
        $uri = trim($uri, '/');
994
        /** @var Element $element */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
995
        $enabledOnly = !Seomatic::$previewingMetaContainers;
996
        $element = Craft::$app->getElements()->getElementByUri($uri, $siteId, $enabledOnly);
997
        if ($element && ($element->uri !== null)) {
998
            Seomatic::setMatchedElement($element);
999
        }
1000
    }
1001
1002
    /**
1003
     * Generate an md5 hash from an object or array
1004
     *
1005
     * @param string|array|MetaItem $data
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
1006
     *
1007
     * @return string
1008
     */
1009
    protected function getHash($data): string
1010
    {
1011
        if (\is_object($data)) {
1012
            $data = $data->toArray();
1013
        }
1014
        if (\is_array($data)) {
1015
            $data = serialize($data);
1016
        }
1017
1018
        return md5($data);
1019
    }
1020
1021
    // Private Methods
1022
    // =========================================================================
1023
}
1024