Passed
Push — develop ( a324ef...3bd85d )
by Andrew
09:43
created

MetaContainers::getContainersOfType()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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