Passed
Push — v4 ( b636d0...2d2527 )
by Andrew
39:55 queued 27:10
created

MetaBundles::invalidateMetaBundleByElement()   D

Complexity

Conditions 18
Paths 98

Size

Total Lines 60
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 342

Importance

Changes 3
Bugs 2 Features 0
Metric Value
eloc 44
dl 0
loc 60
ccs 0
cts 44
cp 0
rs 4.8666
c 3
b 2
f 0
cc 18
nc 98
nop 2
crap 342

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS 3.x
4
 *
5
 * A turnkey SEO implementation for Craft CMS that is comprehensive, powerful,
6
 * and flexible
7
 *
8
 * @link      https://nystudio107.com
9
 * @copyright Copyright (c) 2017 nystudio107
10
 */
11
12
namespace nystudio107\seomatic\services;
13
14
use Craft;
15
use craft\base\Component;
16
use craft\base\Element;
17
use craft\base\Model;
18
use craft\commerce\models\ProductType;
19
use craft\db\Query;
20
use craft\models\CategoryGroup;
21
use craft\models\Section;
22
use craft\models\Section_SiteSettings;
23
use craft\models\Site;
24
use DateTime;
25
use Exception;
26
use nystudio107\seomatic\base\SeoElementInterface;
27
use nystudio107\seomatic\fields\SeoSettings;
28
use nystudio107\seomatic\helpers\ArrayHelper;
29
use nystudio107\seomatic\helpers\Config as ConfigHelper;
30
use nystudio107\seomatic\helpers\MetaValue as MetaValueHelper;
31
use nystudio107\seomatic\helpers\Migration as MigrationHelper;
32
use nystudio107\seomatic\helpers\SiteHelper;
33
use nystudio107\seomatic\models\MetaBundle;
34
use nystudio107\seomatic\models\MetaScriptContainer;
35
use nystudio107\seomatic\models\MetaTagContainer;
36
use nystudio107\seomatic\records\MetaBundle as MetaBundleRecord;
37
use nystudio107\seomatic\Seomatic;
38
use nystudio107\seomatic\services\Tag as TagService;
39
use Throwable;
40
use function in_array;
41
42
/**
43
 * Meta bundle functions for SEOmatic
44
 * An instance of the service is available via [[`Seomatic::$plugin->metaBundles`|`seomatic.bundles`]]
45
 *
46
 * @author    nystudio107Meta bundle failed validation
47
 * @package   Seomatic
48
 * @since     3.0.0
49
 */
50
class MetaBundles extends Component
51
{
52
    // Constants
53
    // =========================================================================
54
55
    const GLOBAL_META_BUNDLE = '__GLOBAL_BUNDLE__';
56
    const FIELD_META_BUNDLE = 'field';
57
58
    const IGNORE_DB_ATTRIBUTES = [
59
        'id',
60
        'dateCreated',
61
        'dateUpdated',
62
        'uid',
63
    ];
64
65
    const ALWAYS_INCLUDED_SEO_SETTINGS_FIELDS = [
66
        'twitterTitle',
67
        'twitterDescription',
68
        'twitterImage',
69
        'twitterImageDescription',
70
71
        'ogTitle',
72
        'ogDescription',
73
        'ogImage',
74
        'ogImageDescription',
75
    ];
76
77
    const COMPOSITE_INHERITANCE_CHILDREN = [
78
        'seoImage' => [
79
            'metaBundleSettings.seoImageTransformMode',
80
            'metaBundleSettings.seoImageTransform',
81
            'metaBundleSettings.seoImageSource',
82
            'metaBundleSettings.seoImageField',
83
            'metaBundleSettings.seoImageIds',
84
        ],
85
        'ogImage' => [
86
            'metaBundleSettings.ogImageTransformMode',
87
            'metaBundleSettings.ogImageTransform',
88
            'metaBundleSettings.ogImageSource',
89
            'metaBundleSettings.ogImageField',
90
            'metaBundleSettings.ogImageIds',
91
        ],
92
        'twitterImage' => [
93
            'metaBundleSettings.twitterImageTransformMode',
94
            'metaBundleSettings.twitterImageTransform',
95
            'metaBundleSettings.twitterImageSource',
96
            'metaBundleSettings.twitterImageField',
97
            'metaBundleSettings.twitterImageIds',
98
        ],
99
    ];
100
101
    // Protected Properties
102
    // =========================================================================
103
104
    /**
105
     * @var MetaBundle[] indexed by [id]
106
     */
107
    protected $metaBundles = [];
108
109
    /**
110
     * @var array indexed by [sourceId][sourceSiteId] = id
111
     */
112
    protected $metaBundlesBySourceId = [];
113
114
    /**
115
     * @var array indexed by [sourceHandle][sourceSiteId] = id
116
     */
117
    protected $metaBundlesBySourceHandle = [];
118
119
    /**
120
     * @var array indexed by [sourceSiteId] = id
121
     */
122
    protected $globalMetaBundles = [];
123
124
    /**
125
     * @var array parent meta bundles for elements
126
     */
127
    protected $elementContentMetaBundles = [];
128
129
    // Public Methods
130
    // =========================================================================
131
132
    /**
133
     * Get the global meta bundle for the site
134
     *
135
     * @param int $sourceSiteId
136
     * @param bool $parse Whether the resulting metabundle should be parsed
137
     *
138
     * @return null|MetaBundle
139
     */
140
    public function getGlobalMetaBundle(int $sourceSiteId, $parse = true)
141
    {
142
        $metaBundle = null;
143
        // See if we have the meta bundle cached
144
        if (!empty($this->globalMetaBundles[$sourceSiteId])) {
145
            return $this->globalMetaBundles[$sourceSiteId];
146
        }
147
        $metaBundleArray = (new Query())
148
            ->from(['{{%seomatic_metabundles}}'])
149
            ->where([
150
                'sourceBundleType' => self::GLOBAL_META_BUNDLE,
151
                'sourceSiteId' => $sourceSiteId,
152
            ])
153
            ->one();
154
        if (!empty($metaBundleArray)) {
155
            // Get the attributes from the db
156
            $metaBundleArray = array_diff_key($metaBundleArray, array_flip(self::IGNORE_DB_ATTRIBUTES));
157
            $metaBundle = MetaBundle::create($metaBundleArray, $parse);
158
            if ($parse) {
159
                $this->syncBundleWithConfig($metaBundle);
160
            }
161
        } else {
162
            // If it doesn't exist, create it
163
            $metaBundle = $this->createGlobalMetaBundleForSite($sourceSiteId);
164
        }
165
        if ($parse) {
166
            // Cache it for future accesses
167
            $this->globalMetaBundles[$sourceSiteId] = $metaBundle;
168
        }
169
170
        return $metaBundle;
171
    }
172
173
    /**
174
     * Synchronize the passed in metaBundle with the seomatic-config files if
175
     * there is a newer version of the MetaBundle bundleVersion in the config
176
     * file
177
     *
178
     * @param MetaBundle $metaBundle
179
     * @param bool $forceUpdate
180
     */
181
    public function syncBundleWithConfig(MetaBundle &$metaBundle, bool $forceUpdate = false)
182
    {
183
        $prevMetaBundle = $metaBundle;
184
        $config = [];
185
        $sourceBundleType = $metaBundle->sourceBundleType;
186
        if ($sourceBundleType === self::GLOBAL_META_BUNDLE) {
187
            $config = ConfigHelper::getConfigFromFile('globalmeta/Bundle');
188
        }
189
        $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($sourceBundleType);
190
        if ($seoElement) {
191
            $configPath = $seoElement::configFilePath();
192
            $config = ConfigHelper::getConfigFromFile($configPath);
193
        }
194
        // If the config file has a newer version than the $metaBundleArray, merge them
195
        $shouldUpdate = !empty($config) && version_compare($config['bundleVersion'], $metaBundle->bundleVersion, '>');
196
        if ($shouldUpdate || $forceUpdate) {
197
            // Create a new meta bundle
198
            if ($sourceBundleType === self::GLOBAL_META_BUNDLE) {
199
                $metaBundle = $this->createGlobalMetaBundleForSite(
200
                    $metaBundle->sourceSiteId,
201
                    $metaBundle
202
                );
203
            } else {
204
                $sourceModel = $seoElement::sourceModelFromId($metaBundle->sourceId);
205
                if ($sourceModel) {
206
                    $metaBundle = $this->createMetaBundleFromSeoElement(
207
                        $seoElement,
208
                        $sourceModel,
209
                        $metaBundle->sourceSiteId,
210
                        $metaBundle
211
                    );
212
                }
213
            }
214
        }
215
216
        // If for some reason we were unable to sync this meta bundle, return the old one
217
        if ($metaBundle === null) {
218
            $metaBundle = $prevMetaBundle;
219
        }
220
    }
221
222
    /**
223
     * @param int $siteId
224
     * @param MetaBundle|null $baseConfig
225
     *
226
     * @return MetaBundle
227
     */
228
    public function createGlobalMetaBundleForSite(int $siteId, $baseConfig = null): MetaBundle
229
    {
230
        // Create a new meta bundle with propagated defaults
231
        $metaBundleDefaults = ArrayHelper::merge(
232
            ConfigHelper::getConfigFromFile('globalmeta/Bundle'),
233
            [
234
                'sourceSiteId' => $siteId,
235
            ]
236
        );
237
        // The computedType must be set before creating the bundle
238
        if ($baseConfig !== null) {
239
            $metaBundleDefaults['metaGlobalVars']['mainEntityOfPage'] = $baseConfig->metaGlobalVars->mainEntityOfPage;
240
            $metaBundleDefaults['metaSiteVars']['identity']['computedType'] =
241
                $baseConfig->metaSiteVars->identity->computedType;
242
            $metaBundleDefaults['metaSiteVars']['creator']['computedType'] =
243
                $baseConfig->metaSiteVars->creator->computedType;
244
        }
245
        $metaBundle = MetaBundle::create($metaBundleDefaults);
246
        if ($metaBundle !== null) {
247
            if ($baseConfig !== null) {
248
                $this->mergeMetaBundleSettings($metaBundle, $baseConfig);
249
            }
250
            $this->updateMetaBundle($metaBundle, $siteId);
251
        }
252
253
        return $metaBundle;
254
    }
255
256
    /**
257
     * @param MetaBundle $metaBundle
258
     * @param int $siteId
259
     */
260
    public function updateMetaBundle(MetaBundle $metaBundle, int $siteId)
261
    {
262
        $metaBundle->sourceName = (string)$metaBundle->sourceName;
263
        $metaBundle->sourceTemplate = (string)$metaBundle->sourceTemplate;
264
        // Make sure it validates
265
        if ($metaBundle->validate(null, true)) {
266
            // Save it out to a record
267
            $params = [
268
                'sourceBundleType' => $metaBundle->sourceBundleType,
269
                'sourceId' => $metaBundle->sourceId,
270
                'sourceSiteId' => $siteId,
271
            ];
272
            if ($metaBundle->typeId !== null) {
273
                $metaBundle->typeId = (int)$metaBundle->typeId;
274
            }
275
            if (!empty($metaBundle->typeId)) {
276
                $params['typeId'] = $metaBundle->typeId;
277
            } else {
278
                $metaBundle->typeId = null;
279
            }
280
            $metaBundleRecord = MetaBundleRecord::findOne($params);
281
282
            if (!$metaBundleRecord) {
0 ignored issues
show
introduced by
$metaBundleRecord is of type yii\db\ActiveRecord, thus it always evaluated to true.
Loading history...
283
                $metaBundleRecord = new MetaBundleRecord();
284
            }
285
286
            // @TODO remove this hack that doesn't allow environment-transformed settings to be saved in a meta bundle with a proper system to address it
287
            // The issue was that the containers were getting saved to the db with a hard-coded setting in them, because they'd
288
            // been set that way by the environment, whereas to be changeable via the GUI, it needs to be set to {seomatic.meta.robots}
289
            $robotsTag = $metaBundle->metaContainers[MetaTagContainer::CONTAINER_TYPE . TagService::GENERAL_HANDLE]->data['robots'] ?? null;
290
            if (!empty($robotsTag)) {
291
                $robotsTag->content = $robotsTag->environment['live']['content'] ?? '{{ seomatic.meta.robots }}';
292
            }
293
294
            $metaBundleRecord->setAttributes($metaBundle->getAttributes(), false);
295
296
            if ($metaBundleRecord->save()) {
297
                Craft::info(
298
                    'Meta bundle updated: '
299
                    . $metaBundle->sourceBundleType
300
                    . ' id: '
301
                    . $metaBundle->sourceId
302
                    . ' from siteId: '
303
                    . $metaBundle->sourceSiteId,
304
                    __METHOD__
305
                );
306
            }
307
        } else {
308
            Craft::error(
309
                'Meta bundle failed validation: '
310
                . print_r($metaBundle->getErrors(), true)
311
                . ' type: '
312
                . $metaBundle->sourceType
313
                . ' id: '
314
                . $metaBundle->sourceId
315
                . ' from siteId: '
316
                . $metaBundle->sourceSiteId,
317
                __METHOD__
318
            );
319
        }
320
    }
321
322
    /**
323
     * @param SeoElementInterface $seoElement
324
     * @param Model $sourceModel
325
     * @param int $sourceSiteId
326
     * @param MetaBundle|null $baseConfig
327
     *
328
     * @return MetaBundle|null
329
     */
330
    public function createMetaBundleFromSeoElement(
331
        $seoElement,
332
        $sourceModel,
333
        int $sourceSiteId,
334
        $baseConfig = null
335
    )
336
    {
337
        $metaBundle = null;
338
        // Get the site settings and turn them into arrays
339
        /** @var Section|CategoryGroup|ProductType $sourceModel */
340
        $siteSettings = $sourceModel->getSiteSettings();
341
        if (!empty($siteSettings[$sourceSiteId])) {
342
            $siteSettingsArray = [];
343
            /** @var Section_SiteSettings $siteSetting */
344
            foreach ($siteSettings as $siteSetting) {
345
                if ($siteSetting->hasUrls && SiteHelper::siteEnabledWithUrls($sourceSiteId)) {
346
                    $siteSettingArray = $siteSetting->toArray();
347
                    // Get the site language
348
                    $siteSettingArray['language'] = MetaValueHelper::getSiteLanguage($siteSetting->siteId);
349
                    $siteSettingsArray[] = $siteSettingArray;
350
                }
351
            }
352
            $siteSettingsArray = ArrayHelper::index($siteSettingsArray, 'siteId');
353
            // Create a MetaBundle for this site
354
            $siteSetting = $siteSettings[$sourceSiteId];
355
            if ($siteSetting->hasUrls && SiteHelper::siteEnabledWithUrls($sourceSiteId)) {
356
                // Get the most recent dateUpdated
357
                $element = $seoElement::mostRecentElement($sourceModel, $sourceSiteId);
358
                /** @var Element $element */
359
                if ($element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
360
                    $dateUpdated = $element->dateUpdated ?? $element->dateCreated;
361
                } else {
362
                    try {
363
                        $dateUpdated = new DateTime();
364
                    } catch (Exception $e) {
365
                    }
366
                }
367
                // Create a new meta bundle with propagated defaults
368
                $metaBundleDefaults = ArrayHelper::merge(
369
                    $seoElement::metaBundleConfig($sourceModel),
370
                    [
371
                        'sourceTemplate' => (string)$siteSetting->template,
372
                        'sourceSiteId' => $siteSetting->siteId,
373
                        'sourceAltSiteSettings' => $siteSettingsArray,
374
                        'sourceDateUpdated' => $dateUpdated,
375
                    ]
376
                );
377
                // The mainEntityOfPage computedType must be set before creating the bundle
378
                if ($baseConfig !== null && !empty($baseConfig->metaGlobalVars->mainEntityOfPage)) {
379
                    $metaBundleDefaults['metaGlobalVars']['mainEntityOfPage'] =
380
                        $baseConfig->metaGlobalVars->mainEntityOfPage;
381
                }
382
                // Merge in any migrated settings from an old Seomatic_Meta Field
383
                if ($element !== null) {
384
                    /** @var Element $elementFromSite */
385
                    $elementFromSite = Craft::$app->getElements()->getElementById($element->id, null, $sourceSiteId);
386
                    if ($element instanceof Element) {
387
                        $config = MigrationHelper::configFromSeomaticMeta(
388
                            $elementFromSite,
389
                            MigrationHelper::SECTION_MIGRATION_CONTEXT
390
                        );
391
                        $metaBundleDefaults = ArrayHelper::merge(
392
                            $metaBundleDefaults,
393
                            $config
394
                        );
395
                    }
396
                }
397
                $metaBundle = MetaBundle::create($metaBundleDefaults);
398
                if ($baseConfig !== null) {
399
                    $this->mergeMetaBundleSettings($metaBundle, $baseConfig);
400
                }
401
                $this->updateMetaBundle($metaBundle, $sourceSiteId);
402
            }
403
        }
404
405
        return $metaBundle;
406
    }
407
408
    /**
409
     * @param string $sourceBundleType
410
     * @param string $sourceHandle
411
     * @param int $sourceSiteId
412
     * @param int|null $typeId
413
     *
414
     * @return null|MetaBundle
415
     */
416
    public function getMetaBundleBySourceHandle(string $sourceBundleType, string $sourceHandle, int $sourceSiteId, $typeId = null)
417
    {
418
        $metaBundle = null;
419
        $typeId = (int)$typeId;
420
        // See if we have the meta bundle cached
421
        if (!empty($this->metaBundlesBySourceHandle[$sourceBundleType][$sourceHandle][$sourceSiteId][$typeId])) {
422
            $id = $this->metaBundlesBySourceHandle[$sourceBundleType][$sourceHandle][$sourceSiteId][$typeId];
423
            if (!empty($this->metaBundles[$id])) {
424
                return $this->metaBundles[$id];
425
            }
426
        }
427
        // Look for a matching meta bundle in the db
428
        $query = (new Query())
429
            ->from(['{{%seomatic_metabundles}}'])
430
            ->where([
431
                'sourceBundleType' => $sourceBundleType,
432
                'sourceHandle' => $sourceHandle,
433
                'sourceSiteId' => $sourceSiteId,
434
            ]);
435
        if (!empty($typeId)) {
436
            $query
437
                ->andWhere([
438
                    'typeId' => $typeId,
439
                ]);
440
        }
441
        $metaBundleArray = $query
442
            ->one();
443
        // If the specific query with a `typeId` returned nothing, try a more general query without `typeId`
444
        if (empty($metaBundleArray)) {
445
            $metaBundleArray = (new Query())
446
                ->from(['{{%seomatic_metabundles}}'])
447
                ->where([
448
                    'sourceBundleType' => $sourceBundleType,
449
                    'sourceHandle' => $sourceHandle,
450
                    'sourceSiteId' => $sourceSiteId,
451
                ])
452
                ->one();
453
        }
454
        if (!empty($metaBundleArray)) {
455
            $metaBundleArray = array_diff_key($metaBundleArray, array_flip(self::IGNORE_DB_ATTRIBUTES));
456
            $metaBundle = MetaBundle::create($metaBundleArray);
457
            $id = count($this->metaBundles);
458
            $this->metaBundles[$id] = $metaBundle;
459
            $this->metaBundlesBySourceHandle[$sourceBundleType][$sourceHandle][$sourceSiteId][$typeId] = $id;
460
        } else {
461
            // If it doesn't exist, create it
462
            $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($sourceBundleType);
463
            if ($seoElement !== null) {
464
                $sourceModel = $seoElement::sourceModelFromHandle($sourceHandle);
465
                if ($sourceModel) {
466
                    $metaBundle = $this->createMetaBundleFromSeoElement($seoElement, $sourceModel, $sourceSiteId);
467
                }
468
            }
469
        }
470
471
        return $metaBundle;
472
    }
473
474
    /**
475
     * Invalidate the caches and data structures associated with this MetaBundle
476
     *
477
     * @param string $sourceBundleType
478
     * @param int|null $sourceId
479
     * @param bool $isNew
480
     */
481
    public function invalidateMetaBundleById(string $sourceBundleType, int $sourceId, bool $isNew = false)
482
    {
483
        $metaBundleInvalidated = false;
484
        $sites = Craft::$app->getSites()->getAllSites();
485
        foreach ($sites as $site) {
486
            // See if this is a section we are tracking
487
            $metaBundle = $this->getMetaBundleBySourceId($sourceBundleType, $sourceId, $site->id);
488
            if ($metaBundle) {
489
                Craft::info(
490
                    'Invalidating meta bundle: '
491
                    . $metaBundle->sourceHandle
492
                    . ' from siteId: '
493
                    . $site->id,
494
                    __METHOD__
495
                );
496
                // Is this a new source?
497
                if (!$isNew) {
498
                    $metaBundleInvalidated = true;
499
                    // Handle syncing up the sourceHandle
500
                    if ($sourceBundleType !== self::GLOBAL_META_BUNDLE) {
501
                        $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($sourceBundleType);
502
                        if ($seoElement !== null) {
503
                            /** @var Section|CategoryGroup|ProductType $sourceModel */
504
                            $sourceModel = $seoElement::sourceModelFromId($sourceId);
505
                            if ($sourceModel !== null) {
506
                                $metaBundle->sourceName = (string)$sourceModel->name;
507
                                $metaBundle->sourceHandle = $sourceModel->handle;
508
                            }
509
                        }
510
                    }
511
                    // Invalidate caches after an existing section is saved
512
                    Seomatic::$plugin->metaContainers->invalidateContainerCacheById(
513
                        $sourceId,
514
                        $sourceBundleType,
515
                        $metaBundle->sourceSiteId
516
                    );
517
                    if (Seomatic::$settings->regenerateSitemapsAutomatically) {
518
                        Seomatic::$plugin->sitemaps->invalidateSitemapCache(
519
                            $metaBundle->sourceHandle,
520
                            $metaBundle->sourceSiteId,
521
                            $metaBundle->sourceBundleType,
522
                            false
523
                        );
524
                    }
525
                    // Update the meta bundle data
526
                    $this->updateMetaBundle($metaBundle, $site->id);
527
                }
528
            }
529
        }
530
        // If we've invalidated a meta bundle, we need to invalidate the sitemap index, too
531
        if ($metaBundleInvalidated) {
532
            Seomatic::$plugin->sitemaps->invalidateSitemapIndexCache();
533
        }
534
    }
535
536
    /**
537
     * @param string $sourceBundleType
538
     * @param int $sourceId
539
     * @param int|null $sourceSiteId
540
     * @param int|null $typeId
541
     *
542
     * @return null|MetaBundle
543
     */
544
    public function getMetaBundleBySourceId(string $sourceBundleType, int $sourceId, int $sourceSiteId, $typeId = null)
545
    {
546
        $metaBundle = null;
547
        $typeId = (int)$typeId;
548
        // See if we have the meta bundle cached
549
        if (!empty($this->metaBundlesBySourceId[$sourceBundleType][$sourceId][$sourceSiteId][$typeId])) {
550
            $id = $this->metaBundlesBySourceId[$sourceBundleType][$sourceId][$sourceSiteId][$typeId];
551
            if (!empty($this->metaBundles[$id])) {
552
                return $this->metaBundles[$id];
553
            }
554
        }
555
        // Look for a matching meta bundle in the db
556
        $query = (new Query())
557
            ->from(['{{%seomatic_metabundles}}'])
558
            ->where([
559
                'sourceBundleType' => $sourceBundleType,
560
                'sourceId' => $sourceId,
561
                'sourceSiteId' => $sourceSiteId,
562
            ]);
563
        if (!empty($typeId)) {
564
            $query
565
                ->andWhere([
566
                    'typeId' => $typeId,
567
                ]);
568
        }
569
        $metaBundleArray = $query
570
            ->one();
571
        // If the specific query with a `typeId` returned nothing, try a more general query without `typeId`
572
        if (empty($metaBundleArray)) {
573
            $metaBundleArray = (new Query())
574
                ->from(['{{%seomatic_metabundles}}'])
575
                ->where([
576
                    'sourceBundleType' => $sourceBundleType,
577
                    'sourceId' => $sourceId,
578
                    'sourceSiteId' => $sourceSiteId,
579
                ])
580
                ->one();
581
        }
582
        if (!empty($metaBundleArray)) {
583
            // Get the attributes from the db
584
            $metaBundleArray = array_diff_key($metaBundleArray, array_flip(self::IGNORE_DB_ATTRIBUTES));
585
            $metaBundle = MetaBundle::create($metaBundleArray);
586
            $this->syncBundleWithConfig($metaBundle);
587
            $id = count($this->metaBundles);
588
            $this->metaBundles[$id] = $metaBundle;
589
            $this->metaBundlesBySourceId[$sourceBundleType][$sourceId][$sourceSiteId][$typeId] = $id;
590
        } else {
591
            // If it doesn't exist, create it
592
            $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($sourceBundleType);
593
            if ($seoElement !== null) {
594
                $sourceModel = $seoElement::sourceModelFromId($sourceId);
595
                if ($sourceModel) {
596
                    $metaBundle = $this->createMetaBundleFromSeoElement($seoElement, $sourceModel, $sourceSiteId);
597
                }
598
            }
599
        }
600
601
        return $metaBundle;
602
    }
603
604
    /**
605
     * Resave all the meta bundles of a given type.
606
     *
607
     * @param string $metaBundleType
608
     */
609
    public function resaveMetaBundles(string $metaBundleType)
610
    {
611
        // For all meta bundles of a given type
612
        $metaBundleRows = (new Query())
613
            ->from(['{{%seomatic_metabundles}}'])
614
            ->where(['sourceBundleType' => $metaBundleType])
615
            ->all();
616
617
        foreach ($metaBundleRows as $metaBundleRow) {
618
            // Create it from the DB data
619
            $metaBundleData = array_diff_key($metaBundleRow, array_flip(self::IGNORE_DB_ATTRIBUTES));
620
            $metaBundle = MetaBundle::create($metaBundleData);
621
            if (!$metaBundle) {
622
                continue;
623
            }
624
            // Sync it and update it.
625
            Seomatic::$plugin->metaBundles->syncBundleWithConfig($metaBundle, true);
626
            Seomatic::$plugin->metaBundles->updateMetaBundle($metaBundle, $metaBundle->sourceSiteId);
627
        }
628
    }
629
630
    /**
631
     * Invalidate the caches and data structures associated with this MetaBundle
632
     *
633
     * @param Element $element
634
     * @param bool $isNew
635
     */
636
    public function invalidateMetaBundleByElement($element, bool $isNew = false)
637
    {
638
        $metaBundleInvalidated = false;
639
        $invalidateMetaBundle = true;
640
        $sitemapInvalidated = false;
641
        if ($element->getIsDraft() || $element->getIsRevision()) {
642
            $invalidateMetaBundle = false;
643
        }
644
        if ($element && $invalidateMetaBundle) {
645
            $uri = $element->uri ?? '';
646
            // Normalize the incoming URI to account for `__home__`
647
            if ($element->slug) {
648
                $uri = ($element->slug === '__home__') ? '' : $uri;
649
            }
650
            // Invalidate sitemap caches after an existing element is saved
651
            list($sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId, $typeId)
652
                = $this->getMetaSourceFromElement($element);
653
            if ($sourceId) {
654
                Craft::info(
655
                    'Invalidating meta bundle: '
656
                    . $uri
657
                    . '/'
658
                    . $sourceSiteId,
659
                    __METHOD__
660
                );
661
                $metaBundleInvalidated = true;
662
                Seomatic::$plugin->metaContainers->invalidateContainerCacheByPath($uri, $sourceSiteId);
663
                // Invalidate the sitemap cache
664
                $metaBundle = $this->getMetaBundleBySourceId($sourceBundleType, $sourceId, $sourceSiteId);
665
                if ($metaBundle) {
666
                    if ($element) {
667
                        $dateUpdated = $element->dateUpdated ?? $element->dateCreated;
668
                    } else {
669
                        try {
670
                            $dateUpdated = new DateTime();
671
                        } catch (Exception $e) {
672
                        }
673
                    }
674
                    $metaBundle->sourceDateUpdated = $dateUpdated;
675
                    // Update the meta bundle data
676
                    $this->updateMetaBundle($metaBundle, $sourceSiteId);
677
                    if ($metaBundle
678
                        && $metaBundle->metaSitemapVars->sitemapUrls
679
                        && $element->scenario !== Element::SCENARIO_ESSENTIALS
680
                        && Seomatic::$settings->regenerateSitemapsAutomatically) {
681
                        $sitemapInvalidated = true;
682
                        Seomatic::$plugin->sitemaps->invalidateSitemapCache(
683
                            $metaBundle->sourceHandle,
684
                            $metaBundle->sourceSiteId,
685
                            $metaBundle->sourceBundleType,
686
                            false
687
                        );
688
                    }
689
                }
690
            }
691
            // If we've invalidated a meta bundle, we need to invalidate the sitemap index, too
692
            if ($metaBundleInvalidated
693
                && $sitemapInvalidated
694
                && $element->scenario !== Element::SCENARIO_ESSENTIALS) {
695
                Seomatic::$plugin->sitemaps->invalidateSitemapIndexCache();
696
            }
697
        }
698
    }
699
700
    /**
701
     * @param Element $element
702
     *
703
     * @return array
704
     */
705
    public function getMetaSourceFromElement(Element $element): array
706
    {
707
        $sourceId = 0;
708
        $typeId = null;
709
        $sourceSiteId = 0;
710
        $sourceHandle = '';
711
        // See if this is a section we are tracking
712
        $sourceBundleType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element);
713
        if ($sourceBundleType) {
714
            $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($sourceBundleType);
715
            if ($seoElement) {
716
                $sourceId = $seoElement::sourceIdFromElement($element);
717
                $typeId = $seoElement::typeIdFromElement($element);
718
                $sourceHandle = $seoElement::sourceHandleFromElement($element);
719
                $sourceSiteId = $element->siteId;
720
            }
721
        } else {
722
            $sourceBundleType = '';
723
        }
724
725
        return [$sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId, $typeId];
726
    }
727
728
    /**
729
     * Get all of the meta bundles for a given $sourceSiteId
730
     *
731
     * @param int|null $sourceSiteId
732
     *
733
     * @return array
734
     */
735
    public function getContentMetaBundlesForSiteId($sourceSiteId, $filter = ''): array
736
    {
737
        $metaBundles = [];
738
        $bundles = [];
739
        // Since sectionIds, CategoryIds, etc. are not unique, we need to do separate queries and combine them
740
        $seoElements = Seomatic::$plugin->seoElements->getAllSeoElementTypes();
741
        foreach ($seoElements as $seoElement) {
742
743
            $subQuery = (new Query())
744
                ->from(['{{%seomatic_metabundles}}'])
745
                ->where(['=', 'sourceBundleType', $seoElement::META_BUNDLE_TYPE]);
746
747
            if ((int)$sourceSiteId !== 0) {
748
                $subQuery->andWhere(['sourceSiteId' => $sourceSiteId]);
749
            }
750
            if ($filter !== '') {
751
                $subQuery->andWhere(['like', 'sourceName', $filter]);
752
            }
753
            $bundleQuery = (new Query())
754
                ->select(['mb.*'])
755
                ->from(['mb' => $subQuery])
756
                ->leftJoin(['mb2' => $subQuery], [
757
                    'and',
758
                    '[[mb.sourceId]] = [[mb2.sourceId]]',
759
                    '[[mb.id]] < [[mb2.id]]'
760
                ])
761
                ->where(['mb2.id' => null]);
762
            $bundles = array_merge($bundles, $bundleQuery->all());
763
        }
764
        foreach ($bundles as $bundle) {
765
            $bundle = array_diff_key($bundle, array_flip(self::IGNORE_DB_ATTRIBUTES));
766
            $metaBundle = MetaBundle::create($bundle);
767
            if ($metaBundle) {
768
                $metaBundles[] = $metaBundle;
769
            }
770
        }
771
772
        return $metaBundles;
773
    }
774
775
    /**
776
     * Get the parent content meta bundle for a given element.
777
     *
778
     * @param Element $element
779
     * @return mixed|MetaBundle|null
780
     */
781
    public function getContentMetaBundleForElement(Element $element)
782
    {
783
        $source = $this->getMetaSourceFromElement($element);
784
        $key = implode(".", $source) . '.' . $element->siteId;
785
786
        if (empty($this->elementContentMetaBundles[$key])) {
787
            $this->elementContentMetaBundles[$key] = $this->getMetaBundleBySourceId($source[1], $source[0], $element->siteId, $source[4]);
788
        }
789
790
        return $this->elementContentMetaBundles[$key];
791
    }
792
793
    /**
794
     * Set fields the user is unable to edit to an empty string, so they are
795
     * filtered out when meta containers are combined
796
     *
797
     * @param MetaBundle $metaBundle
798
     * @param string $fieldHandle
799
     */
800
    public function pruneFieldMetaBundleSettings(MetaBundle $metaBundle, string $fieldHandle)
801
    {
802
        /** @var SeoSettings $seoSettingsField */
803
        $seoSettingsField = Craft::$app->getFields()->getFieldByHandle($fieldHandle);
804
        if ($seoSettingsField) {
805
            $seoSettingsEnabledFields = array_flip(array_merge(
806
                (array)$seoSettingsField->generalEnabledFields,
807
                (array)$seoSettingsField->twitterEnabledFields,
808
                (array)$seoSettingsField->facebookEnabledFields,
809
                (array)$seoSettingsField->sitemapEnabledFields
810
            ));
811
            // Always include some fields, as they are calculated even if not explicitly included
812
            $seoSettingsEnabledFields = array_merge(
813
                $seoSettingsEnabledFields,
814
                array_flip(self::ALWAYS_INCLUDED_SEO_SETTINGS_FIELDS)
815
            );
816
            // metaGlobalVars
817
            $attributes = $metaBundle->metaGlobalVars->getAttributes();
818
819
            // Get a list of explicitly inherited values
820
            $inherited = array_keys(ArrayHelper::remove($attributes, 'inherited', []));
821
            $emptyValues = array_fill_keys(array_keys(array_diff_key($attributes, $seoSettingsEnabledFields)), '');
822
823
            // Nullify the inherited values
824
            $emptyValues = array_merge($emptyValues, array_fill_keys($inherited, ''));
825
            foreach ($inherited as $inheritedAttribute) {
826
                foreach (self::COMPOSITE_INHERITANCE_CHILDREN[$inheritedAttribute] ?? [] as $child) {
827
                    list ($model, $attribute) = explode('.', $child);
828
                    $metaBundle->{$model}->$attribute = '';
829
                }
830
            }
831
832
            $attributes = array_merge($attributes, $emptyValues);
833
            $metaBundle->metaGlobalVars->setAttributes($attributes, false);
834
835
836
            // Handle the mainEntityOfPage
837
            if (!in_array('mainEntityOfPage', (array)$seoSettingsField->generalEnabledFields, false)) {
838
                $metaBundle->metaGlobalVars->mainEntityOfPage = '';
839
            }
840
            // metaSiteVars
841
            $attributes = $metaBundle->metaSiteVars->getAttributes();
842
            $emptyValues = array_fill_keys(array_keys(array_diff_key($attributes, $seoSettingsEnabledFields)), '');
843
            $attributes = array_merge($attributes, $emptyValues);
844
            $metaBundle->metaSiteVars->setAttributes($attributes, false);
845
            // metaSitemapVars
846
            $attributes = $metaBundle->metaSitemapVars->getAttributes();
847
848
            // Get a list of explicitly inherited values
849
            $inherited = array_keys(ArrayHelper::remove($attributes, 'inherited', []));
850
            $emptyValues = array_fill_keys(array_keys(array_diff_key($attributes, $seoSettingsEnabledFields)), '');
851
852
            // Nullify the inherited values
853
            $emptyValues = array_merge($emptyValues, array_fill_keys($inherited, ''));
854
855
            $attributes = array_merge($attributes, $emptyValues);
856
            $metaBundle->metaSitemapVars->setAttributes($attributes, false);
857
        }
858
    }
859
860
    /**
861
     * Remove any meta bundles from the $metaBundles array that no longer
862
     * correspond with an SeoElement
863
     *
864
     * @param array $metaBundles
865
     */
866
    public function pruneVestigialMetaBundles(array &$metaBundles)
867
    {
868
        foreach ($metaBundles as $key => $metaBundle) {
869
            $prune = $this->pruneVestigialMetaBundle($metaBundle);
870
            /** @var MetaBundle $metaBundle */
871
            if ($prune) {
872
                unset($metaBundles[$key]);
873
            }
874
        }
875
        ArrayHelper::multisort($metaBundles, 'sourceName');
876
    }
877
878
    /**
879
     * Determine whether a given MetaBundle is vestigial or not
880
     *
881
     * @param $metaBundle
882
     *
883
     * @return bool
884
     */
885
    public function pruneVestigialMetaBundle($metaBundle): bool
886
    {
887
        $prune = false;
888
        $sourceBundleType = $metaBundle->sourceBundleType;
889
        if ($sourceBundleType && $sourceBundleType !== self::GLOBAL_META_BUNDLE) {
890
            $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($sourceBundleType);
891
            if ($seoElement) {
892
                $sourceModel = $seoElement::sourceModelFromHandle($metaBundle->sourceHandle);
893
                /** @var Section|CategoryGroup|ProductType $sourceModel */
894
                if ($sourceModel === null) {
895
                    $prune = true;
896
                } else {
897
                    $prune = true;
898
                    $siteSettings = $sourceModel->getSiteSettings();
899
                    if (!empty($siteSettings)) {
900
                        /** @var Section_SiteSettings $siteSetting */
901
                        foreach ($siteSettings as $siteSetting) {
902
                            if ($siteSetting->siteId == $metaBundle->sourceSiteId && $siteSetting->hasUrls && SiteHelper::siteEnabledWithUrls($siteSetting->siteId)) {
903
                                $prune = false;
904
                            }
905
                        }
906
                    }
907
                }
908
            } else {
909
                $prune = true;
910
            }
911
        }
912
913
        return $prune;
914
    }
915
916
    /**
917
     * Delete any meta bundles from the $metaBundles array that no longer
918
     * correspond with an SeoElement
919
     *
920
     * @param array $metaBundles
921
     */
922
    public function deleteVestigialMetaBundles(array $metaBundles)
923
    {
924
        foreach ($metaBundles as $key => $metaBundle) {
925
            $prune = $this->pruneVestigialMetaBundle($metaBundle);
926
            /** @var MetaBundle $metaBundle */
927
            if ($prune) {
928
                $this->deleteMetaBundleBySourceId(
929
                    $metaBundle->sourceBundleType,
930
                    $metaBundle->sourceId,
931
                    $metaBundle->sourceSiteId
932
                );
933
            }
934
        }
935
    }
936
937
    /**
938
     * Delete a meta bundle by $sourceId
939
     *
940
     * @param string $sourceBundleType
941
     * @param int $sourceId
942
     * @param null $siteId
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $siteId is correct as it would always require null to be passed?
Loading history...
943
     */
944
    public function deleteMetaBundleBySourceId(string $sourceBundleType, int $sourceId, $siteId = null)
945
    {
946
        $sites = [];
947
        if ($siteId === null) {
948
            $sites = Craft::$app->getSites()->getAllSites();
949
        } else {
950
            $sites[] = Craft::$app->getSites()->getSiteById($siteId);
951
        }
952
        /** @var  $site Site */
953
        foreach ($sites as $site) {
954
            // Look for a matching meta bundle in the db
955
            $metaBundleRecord = MetaBundleRecord::findOne([
956
                'sourceBundleType' => $sourceBundleType,
957
                'sourceId' => $sourceId,
958
                'sourceSiteId' => $site->id,
959
            ]);
960
961
            if ($metaBundleRecord) {
962
                try {
963
                    $metaBundleRecord->delete();
964
                } catch (Throwable $e) {
965
                    Craft::error($e->getMessage(), __METHOD__);
966
                }
967
                Craft::info(
968
                    'Meta bundle deleted: '
969
                    . $sourceId
970
                    . ' from siteId: '
971
                    . $site->id,
972
                    __METHOD__
973
                );
974
            }
975
        }
976
    }
977
978
    /**
979
     * Get all of the data from $bundle in containers of $type
980
     *
981
     * @param MetaBundle $bundle
982
     * @param string $type
983
     *
984
     * @return array
985
     */
986
    public function getContainerDataFromBundle(MetaBundle $bundle, string $type): array
987
    {
988
        $containerData = [];
989
        foreach ($bundle->metaContainers as $metaContainer) {
990
            if ($metaContainer::CONTAINER_TYPE === $type) {
991
                foreach ($metaContainer->data as $dataHandle => $data) {
992
                    $containerData[$dataHandle] = $data;
993
                }
994
            }
995
        }
996
997
        return $containerData;
998
    }
999
1000
    /**
1001
     * Create all of the content meta bundles
1002
     */
1003
    public function createContentMetaBundles()
1004
    {
1005
        $seoElements = Seomatic::$plugin->seoElements->getAllSeoElementTypes();
1006
        foreach ($seoElements as $seoElement) {
1007
            /** @var SeoElementInterface $seoElement */
1008
            $seoElement::createAllContentMetaBundles();
1009
        }
1010
    }
1011
1012
    /**
1013
     * Create the default global meta bundles
1014
     */
1015
    public function createGlobalMetaBundles()
1016
    {
1017
        $sites = Craft::$app->getSites()->getAllSites();
1018
        foreach ($sites as $site) {
1019
            $this->createGlobalMetaBundleForSite($site->id);
1020
        }
1021
    }
1022
1023
    // Protected Methods
1024
    // =========================================================================
1025
1026
    /**
1027
     * Preserve user settings from the meta bundle when updating it from the
1028
     * config
1029
     *
1030
     * @param MetaBundle $metaBundle The new meta bundle
1031
     * @param MetaBundle $baseConfig The existing meta bundle to preserve
1032
     *                               settings from
1033
     */
1034
    protected function mergeMetaBundleSettings(MetaBundle $metaBundle, MetaBundle $baseConfig)
1035
    {
1036
        // Preserve the metaGlobalVars
1037
        $attributes = $baseConfig->metaGlobalVars->getAttributes();
1038
        $metaBundle->metaGlobalVars->setAttributes($attributes);
1039
        // Preserve the metaSiteVars
1040
        if ($baseConfig->metaSiteVars !== null) {
1041
            $attributes = $baseConfig->metaSiteVars->getAttributes();
1042
            $metaBundle->metaSiteVars->setAttributes($attributes);
1043
            if ($baseConfig->metaSiteVars->identity !== null) {
1044
                $attributes = $baseConfig->metaSiteVars->identity->getAttributes();
1045
                $metaBundle->metaSiteVars->identity->setAttributes($attributes);
1046
            }
1047
            if ($baseConfig->metaSiteVars->creator !== null) {
1048
                $attributes = $baseConfig->metaSiteVars->creator->getAttributes();
1049
                $metaBundle->metaSiteVars->creator->setAttributes($attributes);
1050
            }
1051
        }
1052
        // Preserve the Frontend Templates, but add in any new containers
1053
        foreach ($baseConfig->frontendTemplatesContainer->data as $key => $value) {
1054
            $metaBundle->frontendTemplatesContainer->data[$key] = $value;
1055
        }
1056
        // Preserve the metaSitemapVars
1057
        $attributes = $baseConfig->metaSitemapVars->getAttributes();
1058
        $metaBundle->metaSitemapVars->setAttributes($attributes);
1059
        // Preserve the metaBundleSettings
1060
        $attributes = $baseConfig->metaBundleSettings->getAttributes();
1061
        $metaBundle->metaBundleSettings->setAttributes($attributes);
1062
        // Preserve the Script containers, but add in any new containers
1063
        foreach ($baseConfig->metaContainers as $baseMetaContainerName => $baseMetaContainer) {
1064
            if ($baseMetaContainer::CONTAINER_TYPE === MetaScriptContainer::CONTAINER_TYPE) {
1065
                foreach ($baseMetaContainer->data as $key => $value) {
1066
                    if (!empty($metaBundle->metaContainers[$baseMetaContainerName])) {
1067
                        $metaBundle->metaContainers[$baseMetaContainerName]->data[$key] = $value;
1068
                    }
1069
                }
1070
            }
1071
        }
1072
    }
1073
}
1074