Passed
Push — v3 ( cc2dc5...655881 )
by Andrew
38:15 queued 25:10
created

MetaContainers::previewMetaContainers()   F

Complexity

Conditions 14
Paths 577

Size

Total Lines 63
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 210

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 36
c 1
b 1
f 0
dl 0
loc 63
ccs 0
cts 36
cp 0
rs 2.6875
cc 14
nc 577
nop 4
crap 210

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