Passed
Push — v3 ( 9618fe...01dae8 )
by Andrew
47:57 queued 20:05
created

MetaContainers::loadFieldMetaContainers()   C

Complexity

Conditions 13
Paths 2

Size

Total Lines 53
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 182

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 30
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 53
ccs 0
cts 31
cp 0
crap 182
rs 6.6166

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