Passed
Push — develop-v4 ( 2d4983...bb740f )
by Andrew
23:04
created

MetaBundles::mergeMetaBundleSettings()   C

Complexity

Conditions 12
Paths 120

Size

Total Lines 40
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 156

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 26
c 1
b 1
f 0
dl 0
loc 40
ccs 0
cts 27
cp 0
rs 6.8
cc 12
nc 120
nop 2
crap 156

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