Passed
Push — v3 ( a9bf7a...21fd60 )
by Andrew
38:06 queued 25:35
created

MetaContainers::loadMetaContainers()   C

Complexity

Conditions 12
Paths 181

Size

Total Lines 89
Code Lines 61

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 156

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 61
c 1
b 0
f 0
nc 181
nop 2
dl 0
loc 89
ccs 0
cts 58
cp 0
crap 156
rs 5.8842

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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