Passed
Push — v3 ( e59c9d...9ad5d8 )
by Andrew
48:27 queued 25:26
created

MetaContainers::getMetaContainer()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.0123

Importance

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