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