Passed
Push — develop ( 98711b...d3cd9e )
by Andrew
10:23
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
                DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
228
            } else {
229
                $cache = Craft::$app->getCache();
230
                list($this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers) = $cache->getOrSet(
231
                    self::CACHE_KEY.$cacheKey,
232
                    function () use ($uri, $siteId) {
233
                        Craft::info(
234
                            'Meta container cache miss: '.$uri.'/'.$siteId,
235
                            __METHOD__
236
                        );
237
                        $this->loadGlobalMetaContainers($siteId);
238
                        $this->loadContentMetaContainers();
239
                        $this->loadFieldMetaContainers();
240
                        DynamicMetaHelper::addDynamicMetaToContainers($uri, $siteId);
241
242
                        return [$this->metaGlobalVars, $this->metaSiteVars, $this->metaSitemapVars, $this->metaContainers];
243
                    },
244
                    Seomatic::$cacheDuration,
245
                    $dependency
246
                );
247
            }
248
            Seomatic::$seomaticVariable->init();
249
            MetaValueHelper::cache();
250
            Seomatic::$loadingMetaContainers = false;
251
        }
252
        Craft::endProfile('MetaContainers::loadMetaContainers', __METHOD__);
253
    }
254
255
    /**
256
     * Include any script body HTML
257
     *
258
     * @param int $bodyPosition
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
259
     */
260
    public function includeScriptBodyHtml(int $bodyPosition)
261
    {
262
        Craft::beginProfile('MetaContainers::includeScriptBodyHtml', __METHOD__);
263
        $dependency = $this->containerDependency;
264
        $uniqueKey = $dependency->tags[3] ?? self::GLOBALS_CACHE_KEY;
265
        $uniqueKey .= $bodyPosition;
266
        $scriptData = Craft::$app->getCache()->getOrSet(
267
            self::GLOBALS_CACHE_KEY.$uniqueKey,
268
            function () use ($uniqueKey, $bodyPosition) {
269
                Craft::info(
270
                    self::SCRIPTS_CACHE_KEY.' cache miss: '.$uniqueKey,
271
                    __METHOD__
272
                );
273
                $scriptData = [];
274
                $scriptContainers = $this->getContainersOfType(MetaScriptContainer::CONTAINER_TYPE);
275
                foreach ($scriptContainers as $scriptContainer) {
276
                    /** @var MetaScriptContainer $scriptContainer */
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...
277
                    if ($scriptContainer->include) {
278
                        if ($scriptContainer->prepForInclusion()) {
279
                            foreach ($scriptContainer->data as $metaScript) {
280
                                /** @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...
281
                                if (!empty($metaScript->bodyTemplatePath)
282
                                    && ((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...
283
                                    $scriptData[] = $metaScript->renderBodyHtml();
284
                                }
285
                            }
286
                        }
287
                    }
288
                }
289
290
                return $scriptData;
291
            },
292
            Seomatic::$cacheDuration,
293
            $dependency
294
        );
295
        // Output the script HTML
296
        foreach ($scriptData as $script) {
297
            if (is_string($script) && !empty($script)) {
298
                echo $script;
299
            }
300
        }
301
        Craft::endProfile('MetaContainers::includeScriptBodyHtml', __METHOD__);
302
    }
303
304
    /**
305
     * Include the meta containers
306
     */
307
    public function includeMetaContainers()
308
    {
309
        Craft::beginProfile('MetaContainers::includeMetaContainers', __METHOD__);
310
        // If this page is paginated, we need to factor that into the cache key
311
        // We also need to re-add the hreflangs
312
        if ($this->paginationPage !== '1') {
313
            if (Seomatic::$settings->addHrefLang && Seomatic::$settings->addPaginatedHreflang) {
314
                DynamicMetaHelper::addMetaLinkHrefLang();
315
            }
316
        }
317
        // Add in our http headers
318
        DynamicMetaHelper::includeHttpHeaders();
319
        DynamicMetaHelper::addCspTags();
320
        $this->parseGlobalVars();
321
        foreach ($this->metaContainers as $metaContainer) {
322
            /** @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...
323
            if ($metaContainer->include) {
324
                // Don't cache the rendered result if we're previewing meta containers
325
                if (Seomatic::$previewingMetaContainers) {
326
                    $metaContainer->clearCache = true;
327
                }
328
                $metaContainer->includeMetaData($this->containerDependency);
329
            }
330
        }
331
        Craft::endProfile('MetaContainers::includeMetaContainers', __METHOD__);
332
    }
333
334
    /**
335
     * Parse the global variables
336
     */
337
    public function parseGlobalVars()
338
    {
339
        $dependency = $this->containerDependency;
340
        $uniqueKey = $dependency->tags[3] ?? self::GLOBALS_CACHE_KEY;
341
        list($this->metaGlobalVars, $this->metaSiteVars) = Craft::$app->getCache()->getOrSet(
342
            self::GLOBALS_CACHE_KEY.$uniqueKey,
343
            function () use ($uniqueKey) {
344
                Craft::info(
345
                    self::GLOBALS_CACHE_KEY.' cache miss: '.$uniqueKey,
346
                    __METHOD__
347
                );
348
349
                if ($this->metaGlobalVars) {
350
                    $this->metaGlobalVars->parseProperties();
351
                }
352
                if ($this->metaSiteVars) {
353
                    $this->metaSiteVars->parseProperties();
354
                }
355
356
                return [$this->metaGlobalVars, $this->metaSiteVars];
357
            },
358
            Seomatic::$cacheDuration,
359
            $dependency
360
        );
361
    }
362
363
    /**
364
     * Prep all of the meta for preview purposes
365
     *
366
     * @param string   $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
367
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
368
     * @param bool     $parseVariables Whether or not the variables should be
369
     *                                 parsed as Twig
370
     * @param bool     $includeElement Whether or not the matched element
371
     *                                 should be factored into the preview
372
     */
373
    public function previewMetaContainers(
374
        string $uri = '',
375
        int $siteId = null,
376
        bool $parseVariables = false,
377
        bool $includeElement = true
378
    ) {
379
        // It's possible this won't exist at this point
380
        if (!Seomatic::$seomaticVariable) {
381
            // Create our variable and stash it in the plugin for global access
382
            Seomatic::$seomaticVariable = new SeomaticVariable();
383
        }
384
        Seomatic::$previewingMetaContainers = true;
385
        $this->includeMatchedElement = $includeElement;
386
        $this->loadMetaContainers($uri, $siteId);
387
        // Load in the right globals
388
        $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...
389
        $globalSets = GlobalSet::findAll([
390
            'siteId' => $siteId,
391
        ]);
392
        MetaValueHelper::$templatePreviewVars = [];
393
        foreach ($globalSets as $globalSet) {
394
            MetaValueHelper::$templatePreviewVars[$globalSet->handle] = $globalSet;
395
        }
396
        // Parse the global vars
397
        if ($parseVariables) {
398
            $this->parseGlobalVars();
399
        }
400
        // Get the homeUrl and canonicalUrl
401
        $homeUrl = '/';
402
        $canonicalUrl = DynamicMetaHelper::sanitizeUrl($uri, false);
403
        // Special-case the global bundle
404
        if ($uri === MetaBundles::GLOBAL_META_BUNDLE || $uri === '__home__') {
405
            $canonicalUrl = '/';
406
        }
407
        try {
408
            $homeUrl = UrlHelper::siteUrl($homeUrl, null, null, $siteId);
409
            $canonicalUrl = UrlHelper::siteUrl($canonicalUrl, null, null, $siteId);
410
        } catch (Exception $e) {
411
            Craft::error($e->getMessage(), __METHOD__);
412
        }
413
        $canonical = Seomatic::$seomaticVariable->link->get('canonical');
414
        if ($canonical !== null) {
415
            $canonical->href = $canonicalUrl;
416
        }
417
        $home = Seomatic::$seomaticVariable->link->get('home');
418
        if ($home !== null) {
419
            $home->href = $homeUrl;
420
        }
421
        // The current language may _not_ match the current site, if we're headless
422
        $ogLocale = Seomatic::$seomaticVariable->tag->get('og:locale');
423
        if ($ogLocale !== null && $siteId !== null) {
424
            $site = Craft::$app->getSites()->getSiteById($siteId);
425
            if ($site !== null) {
426
                $ogLocale->content = LocalizationHelper::normalizeOgLocaleLanguage($site->language);
427
            }
428
        }
429
        // Update seomatic.meta.canonicalUrl when previewing meta containers
430
        $this->metaGlobalVars->canonicalUrl = $canonicalUrl;
431
    }
432
433
    /**
434
     * Add the passed in MetaItem to the MetaContainer indexed as $key
435
     *
436
     * @param $data MetaItem The MetaItem to add to the container
437
     * @param $key  string   The key to the container to add the data to
438
     */
439 1
    public function addToMetaContainer(MetaItem $data, string $key)
440
    {
441
        /** @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...
442 1
        $container = $this->getMetaContainer($key);
443
444 1
        if ($container !== null) {
445
            $container->addData($data, $data->key);
446
        }
447 1
    }
448
449
    /**
450
     * @param string $key
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
451
     *
452
     * @return mixed|null
453
     */
454 1
    public function getMetaContainer(string $key)
455
    {
456 1
        if (!$key || empty($this->metaContainers[$key])) {
457 1
            $error = Craft::t(
458 1
                'seomatic',
459 1
                'Meta container with key `{key}` does not exist.',
460 1
                ['key' => $key]
461
            );
462 1
            Craft::error($error, __METHOD__);
463
464 1
            return null;
465
        }
466
467
        return $this->metaContainers[$key];
468
    }
469
470
    /**
471
     * Create a MetaContainer of the given $type with the $key
472
     *
473
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
474
     * @param string $key
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
475
     *
476
     * @return null|MetaContainer
477
     */
478
    public function createMetaContainer(string $type, string $key): MetaContainer
479
    {
480
        /** @var MetaContainer $container */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
481
        $container = null;
482
        if (empty($this->metaContainers[$key])) {
483
            /** @var MetaContainer $className */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
484
            $className = null;
485
            // Create a new container based on the type passed in
486
            switch ($type) {
487
                case MetaTagContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
488
                    $className = MetaTagContainer::class;
489
                    break;
490
                case MetaLinkContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
491
                    $className = MetaLinkContainer::class;
492
                    break;
493
                case MetaScriptContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
494
                    $className = MetaScriptContainer::class;
495
                    break;
496
                case MetaJsonLdContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
497
                    $className = MetaJsonLdContainer::class;
498
                    break;
499
                case MetaTitleContainer::CONTAINER_TYPE:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
500
                    $className = MetaTitleContainer::class;
501
                    break;
502
            }
503
            if ($className) {
504
                $container = $className::create();
505
                if ($container) {
0 ignored issues
show
introduced by
$container is of type nystudio107\seomatic\base\Container, thus it always evaluated to true.
Loading history...
506
                    $this->metaContainers[$key] = $container;
507
                }
508
            }
509
        }
510
511
        /** @var MetaContainer $className */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
512
        return $container;
513
    }
514
515
    /**
516
     * Return the containers of a specific type
517
     *
518
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
519
     *
520
     * @return array
521
     */
522
    public function getContainersOfType(string $type): array
523
    {
524
        $containers = [];
525
        /** @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...
526
        foreach ($this->metaContainers as $metaContainer) {
527
            if ($metaContainer::CONTAINER_TYPE === $type) {
528
                $containers[] = $metaContainer;
529
            }
530
        }
531
532
        return $containers;
533
    }
534
535
    /**
536
     * Render the HTML of all MetaContainers of a specific $type
537
     *
538
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
539
     *
540
     * @return string
541
     */
542
    public function renderContainersByType(string $type): string
543
    {
544
        $html = '';
545
        /** @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...
546
        foreach ($this->metaContainers as $metaContainer) {
547
            if ($metaContainer::CONTAINER_TYPE === $type && $metaContainer->include) {
548
                $result = $metaContainer->render([
549
                    'renderRaw'        => true,
550
                    'renderScriptTags' => true,
551
                    'array'            => true,
552
                ]);
553
                // Special case for script containers, because they can have body scripts too
554
                if ($metaContainer::CONTAINER_TYPE === MetaScriptContainer::CONTAINER_TYPE) {
555
                    $bodyScript = '';
556
                    /** @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...
557
                    if ($metaContainer->prepForInclusion()) {
558
                        foreach ($metaContainer->data as $metaScript) {
559
                            /** @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...
560
                            if (!empty($metaScript->bodyTemplatePath)) {
561
                                $bodyScript .= $metaScript->renderBodyHtml();
562
                            }
563
                        }
564
                    }
565
566
                    $result = Json::encode([
567
                        'script' => $result,
568
                        'bodyScript' => $bodyScript,
569
                    ]);
570
                }
571
572
                $html .= $result;
573
            }
574
        }
575
        // Special-case for requests for the MetaSiteVars "container"
576
        if ($type === MetaSiteVars::CONTAINER_TYPE) {
577
            $result = Json::encode($this->metaSiteVars->toArray());
578
            $html .= $result;
579
        }
580
581
        return $html;
582
    }
583
584
    /**
585
     * Render the HTML of all MetaContainers of a specific $type as an array
586
     *
587
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
588
     *
589
     * @return array
590
     */
591
    public function renderContainersArrayByType(string $type): array
592
    {
593
        $htmlArray = [];
594
        /** @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...
595
        foreach ($this->metaContainers as $metaContainer) {
596
            if ($metaContainer::CONTAINER_TYPE === $type && $metaContainer->include) {
597
                /** @noinspection SlowArrayOperationsInLoopInspection */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
598
                $htmlArray = array_merge($htmlArray, $metaContainer->renderArray());
599
            }
600
        }
601
        // Special-case for requests for the MetaSiteVars "container"
602
        if ($type === MetaSiteVars::CONTAINER_TYPE) {
603
            $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...
604
            $htmlArray = array_merge($htmlArray, $this->metaSiteVars->toArray());
605
        }
606
607
        return $htmlArray;
608
    }
609
610
    /**
611
     * Return a MetaItem object by $key from container $type
612
     *
613
     * @param string $key
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
614
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
615
     *
616
     * @return null|MetaItem
617
     */
618
    public function getMetaItemByKey(string $key, string $type = '')
619
    {
620
        $metaItem = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $metaItem is dead and can be removed.
Loading history...
621
        /** @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...
622
        foreach ($this->metaContainers as $metaContainer) {
623
            if (($metaContainer::CONTAINER_TYPE === $type) || empty($type)) {
624
                /** @var  $metaTag MetaItem */
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...
625
                foreach ($metaContainer->data as $metaItem) {
626
                    if ($key === $metaItem->key) {
627
                        return $metaItem;
628
                    }
629
                }
630
            }
631
        }
632
633
        return null;
634
    }
635
636
    // Protected Methods
637
    // =========================================================================
638
639
    /**
640
     * Return the MetaBundle that corresponds with the Seomatic::$matchedElement
641
     *
642
     * @return null|MetaBundle
643
     */
644
    public function getMatchedMetaBundle()
645
    {
646
        $metaBundle = null;
647
        /** @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...
648
        $element = Seomatic::$matchedElement;
649
        if ($element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
650
            $sourceType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element);
651
            if ($sourceType) {
652
                list($sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId, $typeId)
653
                    = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($element);
654
                $metaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
655
                    $sourceType,
656
                    $sourceId,
657
                    $sourceSiteId,
658
                    $typeId
659
                );
660
            }
661
        }
662
        $this->matchedMetaBundle = $metaBundle;
663
664
        return $metaBundle;
665
    }
666
667
    /**
668
     * Add the meta bundle to our existing meta containers, overwriting meta
669
     * items with the same key
670
     *
671
     * @param MetaBundle $metaBundle
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
672
     */
673
    public function addMetaBundleToContainers(MetaBundle $metaBundle)
674
    {
675
        // Ensure the variable is synced properly first
676
        Seomatic::$seomaticVariable->init();
677
        // Meta global vars
678
        $attributes = $metaBundle->metaGlobalVars->getAttributes();
679
        // Parse the meta values so we can filter out any blank or empty attributes
680
        // So that they can fall back on the parent container
681
        $parsedAttributes = $attributes;
682
        MetaValueHelper::parseArray($parsedAttributes);
683
        $parsedAttributes = array_filter(
684
            $parsedAttributes,
685
            [ArrayHelper::class, 'preserveBools']
686
        );
687
        $attributes = array_intersect_key($attributes, $parsedAttributes);
688
        // Add the attributes in
689
        $attributes = array_filter(
690
            $attributes,
691
            [ArrayHelper::class, 'preserveBools']
692
        );
693
        $this->metaGlobalVars->setAttributes($attributes, false);
694
        // Meta site vars
695
        /*
696
         * Don't merge in the Site vars, since they are only editable on
697
         * a global basis. Otherwise stale data will be unable to be edited
698
        $attributes = $metaBundle->metaSiteVars->getAttributes();
699
        $attributes = array_filter($attributes);
700
        $this->metaSiteVars->setAttributes($attributes, false);
701
        */
702
        // Meta sitemap vars
703
        $attributes = $metaBundle->metaSitemapVars->getAttributes();
704
        $attributes = array_filter(
705
            $attributes,
706
            [ArrayHelper::class, 'preserveBools']
707
        );
708
        $this->metaSitemapVars->setAttributes($attributes, false);
709
        // Language
710
        $this->metaGlobalVars->language = Seomatic::$language;
711
        // Meta containers
712
        foreach ($metaBundle->metaContainers as $key => $metaContainer) {
713
            foreach ($metaContainer->data as $metaTag) {
714
                $this->addToMetaContainer($metaTag, $key);
715
            }
716
        }
717
    }
718
719
    /**
720
     * Invalidate all of the meta container caches
721
     */
722
    public function invalidateCaches()
723
    {
724
        $cache = Craft::$app->getCache();
725
        TagDependency::invalidate($cache, self::GLOBAL_METACONTAINER_CACHE_TAG);
726
        Craft::info(
727
            'All meta container caches cleared',
728
            __METHOD__
729
        );
730
        // Trigger an event to let other plugins/modules know we've cleared our caches
731
        $event = new InvalidateContainerCachesEvent([
732
            'uri' => null,
733
            'siteId' => null,
734
            'sourceId' => null,
735
            'sourceType' => null,
736
        ]);
737
        if (!Craft::$app instanceof ConsoleApplication) {
738
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
739
        }
740
    }
741
742
    /**
743
     * Invalidate a meta bundle cache
744
     *
745
     * @param int          $sourceId
0 ignored issues
show
Coding Style introduced by
Expected 9 spaces after parameter type; 10 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
746
     * @param null|string  $sourceType
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter type; 2 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
747
     * @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...
748
     */
749
    public function invalidateContainerCacheById(int $sourceId, $sourceType = null, $siteId = null)
750
    {
751
        $metaBundleSourceId = '';
752
        if ($sourceId) {
753
            $metaBundleSourceId = $sourceId;
754
        }
755
        $metaBundleSourceType = '';
756
        if ($sourceType) {
757
            $metaBundleSourceType = $sourceType;
758
        }
759
        if ($siteId === null) {
760
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
761
        }
762
        $cache = Craft::$app->getCache();
763
        TagDependency::invalidate(
764
            $cache,
765
            self::METACONTAINER_CACHE_TAG.$metaBundleSourceId.$metaBundleSourceType.$siteId
766
        );
767
        Craft::info(
768
            'Meta bundle cache cleared: '.$metaBundleSourceId.' / '.$metaBundleSourceType.' / '.$siteId,
769
            __METHOD__
770
        );
771
        // Trigger an event to let other plugins/modules know we've cleared our caches
772
        $event = new InvalidateContainerCachesEvent([
773
            'uri' => null,
774
            'siteId' => $siteId,
775
            'sourceId' => $sourceId,
776
            'sourceType' => $metaBundleSourceType,
777
        ]);
778
        if (!Craft::$app instanceof ConsoleApplication) {
779
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
780
        }
781
    }
782
783
    /**
784
     * Invalidate a meta bundle cache
785
     *
786
     * @param string   $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
787
     * @param null|int $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
788
     */
789
    public function invalidateContainerCacheByPath(string $uri, $siteId = null)
790
    {
791
        $cache = Craft::$app->getCache();
792
        if ($siteId === null) {
793
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
794
        }
795
        TagDependency::invalidate($cache, self::METACONTAINER_CACHE_TAG.$uri.$siteId);
796
        Craft::info(
797
            'Meta container cache cleared: '.$uri.' / '.$siteId,
798
            __METHOD__
799
        );
800
        // Trigger an event to let other plugins/modules know we've cleared our caches
801
        $event = new InvalidateContainerCachesEvent([
802
            'uri' => $uri,
803
            'siteId' => $siteId,
804
            'sourceId' => null,
805
            'sourceType' => null,
806
        ]);
807
        if (!Craft::$app instanceof ConsoleApplication) {
808
            $this->trigger(self::EVENT_INVALIDATE_CONTAINER_CACHES, $event);
809
        }
810
    }
811
812
    /**
813
     * Load the global site meta containers
814
     *
815
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
816
     */
817
    public function loadGlobalMetaContainers(int $siteId = null)
818
    {
819
        Craft::beginProfile('MetaContainers::loadGlobalMetaContainers', __METHOD__);
820
        if ($siteId === null) {
821
            $siteId = Craft::$app->getSites()->currentSite->id ?? 1;
822
        }
823
        $metaBundle = Seomatic::$plugin->metaBundles->getGlobalMetaBundle($siteId);
824
        if ($metaBundle) {
825
            // Meta global vars
826
            $this->metaGlobalVars = clone $metaBundle->metaGlobalVars;
827
            // Meta site vars
828
            $this->metaSiteVars = clone $metaBundle->metaSiteVars;
829
            // Meta sitemap vars
830
            $this->metaSitemapVars = clone $metaBundle->metaSitemapVars;
831
            // Language
832
            $this->metaGlobalVars->language = Seomatic::$language;
833
            // Meta containers
834
            foreach ($metaBundle->metaContainers as $key => $metaContainer) {
835
                $this->metaContainers[$key] = clone $metaContainer;
836
            }
837
        }
838
        Craft::beginProfile('MetaContainers::loadGlobalMetaContainers', __METHOD__);
839
    }
840
841
    // Protected Methods
842
    // =========================================================================
843
844
    /**
845
     * Load the meta containers specific to the matched meta bundle
846
     */
847
    protected function loadContentMetaContainers()
848
    {
849
        Craft::beginProfile('MetaContainers::loadContentMetaContainers', __METHOD__);
850
        $metaBundle = $this->getMatchedMetaBundle();
851
        if ($metaBundle) {
852
            $this->addMetaBundleToContainers($metaBundle);
853
        }
854
        Craft::beginProfile('MetaContainers::loadContentMetaContainers', __METHOD__);
855
    }
856
857
    /**
858
     * Load any meta containers in the current element
859
     */
860
    protected function loadFieldMetaContainers()
861
    {
862
        Craft::beginProfile('MetaContainers::loadFieldMetaContainers', __METHOD__);
863
        $element = Seomatic::$matchedElement;
864
        if ($element && $this->includeMatchedElement) {
865
            /** @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...
866
            $fieldHandles = FieldHelper::fieldsOfTypeFromElement($element, FieldHelper::SEO_SETTINGS_CLASS_KEY, true);
867
            foreach ($fieldHandles as $fieldHandle) {
868
                if (!empty($element->$fieldHandle)) {
869
                    /** @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...
870
                    $metaBundle = $element->$fieldHandle;
871
                    Seomatic::$plugin->metaBundles->pruneFieldMetaBundleSettings($metaBundle, $fieldHandle);
872
873
                    // See which properties have to be overridden, because the parent bundle says so.
874
                    foreach (self::COMPOSITE_SETTING_LOOKUP as $settingName => $rules) {
875
                        if (empty($metaBundle->metaGlobalVars->{$settingName})) {
876
                            $parentBundle = Seomatic::$plugin->metaBundles->getContentMetaBundleForElement($element);
877
878
                            foreach ($rules as $settingPath => $action) {
879
                                list ($container, $property) = explode('.', $settingPath);
880
                                list ($testValue, $sourceSetting) = explode('.', $action);
881
882
                                if ($parentBundle->{$container}->{$property} == $testValue) {
883
                                    $metaBundle->metaGlobalVars->{$settingName}  = $metaBundle->metaGlobalVars->{$sourceSetting};
884
                                }
885
                            }
886
                        }
887
                    }
888
889
                    // Handle re-creating the `mainEntityOfPage` so that the model injected into the
890
                    // templates has the appropriate attributes
891
                    $generalContainerKey = MetaJsonLdContainer::CONTAINER_TYPE.JsonLdService::GENERAL_HANDLE;
892
                    $generalContainer = $this->metaContainers[$generalContainerKey];
893
                    if (($generalContainer !== null) && !empty($generalContainer->data['mainEntityOfPage'])) {
894
                        /** @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...
895
                        $jsonLdModel = $generalContainer->data['mainEntityOfPage'];
896
                        $config = $jsonLdModel->getAttributes();
897
                        $schemaType = $metaBundle->metaGlobalVars->mainEntityOfPage ?? $config['type'] ?? null;
898
                        // If the schemaType is '' we should fall back on whatever the mainEntityOfPage already is
899
                        if (empty($schemaType)) {
900
                            $schemaType = null;
901
                        }
902
                        if ($schemaType !== null) {
903
                            $config['key'] = 'mainEntityOfPage';
904
                            $schemaType = MetaValueHelper::parseString($schemaType);
905
                            $generalContainer->data['mainEntityOfPage'] = MetaJsonLd::create($schemaType, $config);
906
                        }
907
                    }
908
                    $this->addMetaBundleToContainers($metaBundle);
909
                }
910
            }
911
        }
912
        Craft::beginProfile('MetaContainers::loadFieldMetaContainers', __METHOD__);
913
    }
914
915
    /**
916
     * Set the element that matches the $uri
917
     *
918
     * @param string   $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
919
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
920
     */
921
    protected function setMatchedElement(string $uri, int $siteId = null)
922
    {
923
        if ($siteId === null) {
924
            $siteId = Craft::$app->getSites()->currentSite->id
925
                ?? Craft::$app->getSites()->primarySite->id
926
                ?? 1;
927
        }
928
        $uri = trim($uri, '/');
929
        /** @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...
930
        $enabledOnly = !Seomatic::$previewingMetaContainers;
931
        $element = Craft::$app->getElements()->getElementByUri($uri, $siteId, $enabledOnly);
932
        if ($element && ($element->uri !== null)) {
933
            Seomatic::setMatchedElement($element);
934
        }
935
    }
936
937
    /**
938
     * Generate an md5 hash from an object or array
939
     *
940
     * @param string|array|MetaItem $data
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
941
     *
942
     * @return string
943
     */
944
    protected function getHash($data): string
945
    {
946
        if (\is_object($data)) {
947
            $data = $data->toArray();
948
        }
949
        if (\is_array($data)) {
950
            $data = serialize($data);
951
        }
952
953
        return md5($data);
954
    }
955
956
    // Private Methods
957
    // =========================================================================
958
}
959