Passed
Push — develop ( db1623...3b1851 )
by Andrew
12:14
created

MetaContainers::invalidateCaches()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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