Passed
Push — develop ( 793d0f...5fa866 )
by Andrew
20:45
created

SeoSettings   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 467
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 230
c 6
b 0
f 0
dl 0
loc 467
ccs 0
cts 316
cp 0
rs 8.64
wmc 47

10 Methods

Rating   Name   Duplication   Size   Complexity  
A useFieldset() 0 3 1
A displayName() 0 3 1
A getTableAttributeHtml() 0 51 5
B getInputHtml() 0 67 5
A rules() 0 32 1
A setContentFieldSourceVariables() 0 33 1
A getContentColumnType() 0 3 1
F normalizeValue() 0 69 15
A getSettingsHtml() 0 37 3
C serializeValue() 0 44 14

How to fix   Complexity   

Complex Class

Complex classes like SeoSettings often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SeoSettings, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS 3.x
4
 *
5
 * @link      https://nystudio107.com/
6
 * @copyright Copyright (c) 2017 nystudio107
7
 * @license   https://nystudio107.com/license
0 ignored issues
show
Coding Style introduced by
@license tag must contain a URL and a license name
Loading history...
8
 */
9
10
namespace nystudio107\seomatic\fields;
11
12
use nystudio107\seomatic\Seomatic;
13
use nystudio107\seomatic\assetbundles\seomatic\SeomaticAsset;
14
use nystudio107\seomatic\helpers\ArrayHelper;
15
use nystudio107\seomatic\helpers\Config as ConfigHelper;
16
use nystudio107\seomatic\helpers\Field as FieldHelper;
17
use nystudio107\seomatic\helpers\ImageTransform as ImageTransformHelper;
18
use nystudio107\seomatic\helpers\Manifest as ManifestHelper;
19
use nystudio107\seomatic\helpers\Migration as MigrationHelper;
20
use nystudio107\seomatic\helpers\PullField as PullFieldHelper;
21
use nystudio107\seomatic\helpers\Schema as SchemaHelper;
22
use nystudio107\seomatic\models\MetaBundle;
23
use nystudio107\seomatic\seoelements\SeoEntry;
24
use nystudio107\seomatic\services\MetaContainers;
25
26
use Craft;
27
use craft\base\Element;
28
use craft\base\ElementInterface;
29
use craft\base\Field;
30
use craft\base\PreviewableFieldInterface;
31
use craft\elements\Asset;
32
use craft\helpers\Json;
33
use craft\helpers\StringHelper;
34
35
use yii\base\InvalidConfigException;
36
use yii\caching\TagDependency;
37
use yii\db\Schema;
38
39
/**
40
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
41
 * @package   Seomatic
42
 * @since     3.0.0
43
 */
44
class SeoSettings extends Field implements PreviewableFieldInterface
45
{
46
    // Constants
47
    // =========================================================================
48
49
    const CACHE_KEY = 'seomatic_fieldmeta_';
50
51
    const BUNDLE_COMPARE_FIELDS = [
52
        'metaGlobalVars',
53
    ];
54
55
    // Public Properties
56
    // =========================================================================
57
58
    /**
59
     * @var string
60
     */
61
    public $elementDisplayPreviewType = 'google';
62
63
    /**
64
     * @var bool
65
     */
66
    public $generalTabEnabled = true;
67
68
    /**
69
     * @var array
70
     */
71
    public $generalEnabledFields = [
72
        'seoTitle',
73
        'seoDescription',
74
        'seoImage',
75
    ];
76
77
    /**
78
     * @var bool
79
     */
80
    public $twitterTabEnabled = false;
81
82
    /**
83
     * @var array
84
     */
85
    public $twitterEnabledFields = [];
86
87
    /**
88
     * @var bool
89
     */
90
    public $facebookTabEnabled = false;
91
92
    /**
93
     * @var array
94
     */
95
    public $facebookEnabledFields = [];
96
97
    /**
98
     * @var bool
99
     */
100
    public $sitemapTabEnabled = false;
101
102
    /**
103
     * @var array
104
     */
105
    public $sitemapEnabledFields = [];
106
107
    // Static Methods
108
    // =========================================================================
109
110
    /**
111
     * @inheritdoc
112
     */
113
    public static function displayName(): string
114
    {
115
        return Craft::t('seomatic', 'SEO Settings');
116
    }
117
118
    // Public Methods
119
    // =========================================================================
120
121
    /**
122
     * @inheritdoc
123
     */
124
    public function rules()
125
    {
126
        $rules = parent::rules();
127
        $rules = array_merge($rules, [
128
            [
129
                [
130
                    'elementDisplayPreviewType',
131
                ],
132
                'string',
133
            ],
134
            [
135
                [
136
                    'generalTabEnabled',
137
                    'twitterTabEnabled',
138
                    'facebookTabEnabled',
139
                    'sitemapTabEnabled',
140
                ],
141
                'boolean',
142
            ],
143
            [
144
                [
145
                    'generalEnabledFields',
146
                    'twitterEnabledFields',
147
                    'facebookEnabledFields',
148
                    'sitemapEnabledFields',
149
                ],
150
                'each', 'rule' => ['string'],
151
            ],
152
153
        ]);
154
155
        return $rules;
156
    }
157
158
    /**
159
     * @inheritdoc
160
     */
161
    public function getContentColumnType(): string
162
    {
163
        return Schema::TYPE_TEXT;
164
    }
165
166
    /**
167
     * @inheritdoc
168
     * @since 2.0.0
169
     */
170
    public function useFieldset(): bool
171
    {
172
        return true;
173
    }
174
175
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $value should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $element should have a doc-comment as per coding-style.
Loading history...
176
     * @inheritdoc
177
     */
178
    public function normalizeValue($value, ElementInterface $element = null)
179
    {
180
        $config = [];
181
        // Handle incoming values potentially being JSON, an array, or an object
182
        if (!empty($value)) {
183
            if (\is_string($value)) {
184
                // Decode any html entities
185
                $value = html_entity_decode($value, ENT_NOQUOTES, 'UTF-8');
186
                $config = Json::decodeIfJson($value);
187
            }
188
            if (\is_array($value)) {
189
                $config = $value;
190
            }
191
            if (\is_object($value) && $value instanceof MetaBundle) {
192
                $config = $value->toArray();
193
            }
194
        } else {
195
            /** @var null|Element $element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
196
            $config = MigrationHelper::configFromSeomaticMeta(
197
                $element,
198
                MigrationHelper::FIELD_MIGRATION_CONTEXT
199
            );
200
        }
201
        // If the config isn't empty, do some processing on the values
202
        if (!empty($config)) {
203
            $elementName = '';
204
            /** @var Element $element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
205
            if ($element !== null) {
206
                try {
207
                    $reflector = new \ReflectionClass($element);
208
                } catch (\ReflectionException $e) {
209
                    $reflector = null;
210
                    Craft::error($e->getMessage(), __METHOD__);
211
                }
212
                if ($reflector) {
213
                    $elementName = strtolower($reflector->getShortName());
214
                }
215
            }
216
            // Handle the pull fields
217
            if (!empty($config['metaGlobalVars']) && !empty($config['metaBundleSettings'])) {
218
                PullFieldHelper::parseTextSources(
219
                    $elementName,
220
                    $config['metaGlobalVars'],
221
                    $config['metaBundleSettings']
222
                );
223
                PullFieldHelper::parseImageSources(
224
                    $elementName,
225
                    $config['metaGlobalVars'],
226
                    $config['metaBundleSettings'],
227
                    null
228
                );
229
            }
230
            // Handle the mainEntityOfPage
231
            $mainEntity = '';
232
            if (\in_array('mainEntityOfPage', $this->generalEnabledFields, false) &&
233
                !empty($config['metaBundleSettings'])) {
0 ignored issues
show
Coding Style introduced by
Each line in a multi-line IF statement must begin with a boolean operator
Loading history...
Coding Style introduced by
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
234
                $mainEntity = SchemaHelper::getSpecificEntityType($config['metaBundleSettings'], true);
235
            }
236
            if (!empty($config['metaGlobalVars'])) {
237
                $config['metaGlobalVars']['mainEntityOfPage'] = $mainEntity;
238
            }
239
        }
240
        // Create a new meta bundle with propagated defaults
241
        $metaBundleDefaults = ArrayHelper::merge(
242
            ConfigHelper::getConfigFromFile('fieldmeta/Bundle'),
243
            $config
244
        );
245
246
        return MetaBundle::create($metaBundleDefaults);
247
    }
248
249
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $value should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $element should have a doc-comment as per coding-style.
Loading history...
250
     * @inheritdoc
251
     */
252
    public function serializeValue($value, ElementInterface $element = null)
253
    {
254
        // If the field replicates values from the Content SEO settings, nullify them
255
        if ($element !== null && $value instanceof MetaBundle && Seomatic::$plugin->migrationsAndSchemaReady()) {
256
            list($sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId, $typeId)
257
                = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($element);
258
            $metaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
259
                $sourceBundleType,
260
                $sourceId,
261
                $sourceSiteId
262
            );
263
            if ($metaBundle) {
264
                foreach (self::BUNDLE_COMPARE_FIELDS as $fieldName) {
265
                    $bundleAttributes = $metaBundle->$fieldName->getAttributes();
266
                    $fieldAttributes = $value->$fieldName->getAttributes();
267
                    if (!empty($bundleAttributes) && !empty($fieldAttributes)) {
268
                        $intersect = array_intersect_assoc($bundleAttributes, $fieldAttributes);
269
                        $intersect = array_filter(
270
                            $intersect,
271
                            [ArrayHelper::class, 'preserveBools']
272
                        );
273
                        foreach ($intersect as $intersectKey => $intersectValue) {
274
                            $value->$fieldName[$intersectKey] = '';
275
                        }
276
                    }
277
                }
278
            }
279
        }
280
        $value = parent::serializeValue($value, $element);
281
        if (!Craft::$app->getDb()->getSupportsMb4()) {
282
            if (\is_string($value)) {
283
                // Encode any 4-byte UTF-8 characters.
284
                $value = StringHelper::encodeMb4($value);
285
            }
286
            if (\is_array($value)) {
287
                array_walk_recursive($value, function (&$arrayValue, $arrayKey) {
0 ignored issues
show
Unused Code introduced by
The parameter $arrayKey is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

287
                array_walk_recursive($value, function (&$arrayValue, /** @scrutinizer ignore-unused */ $arrayKey) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
288
                    if ($arrayValue !== null && \is_string($arrayValue)) {
289
                        $arrayValue = StringHelper::encodeMb4($arrayValue);
290
                    }
291
                });
292
            }
293
        }
294
295
        return $value;
296
    }
297
298
    /**
299
     * @inheritdoc
300
     */
301
    public function getSettingsHtml()
302
    {
303
        $variables = [];
304
        // JS/CSS modules
305
        try {
306
            Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
307
            ManifestHelper::registerCssModules([
308
                'styles.css',
309
                'vendors.css',
310
            ]);
311
            ManifestHelper::registerJsModules([
312
                'runtime.js',
313
                'vendors.js',
314
                'commons.js',
315
                'seomatic.js',
316
                'seomatic-tokens.js',
317
                'seomatic-meta.js',
318
            ]);
319
        } catch (InvalidConfigException $e) {
320
            Craft::error($e->getMessage(), __METHOD__);
321
        }
322
        // Asset bundle
323
        try {
324
            Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
325
        } catch (InvalidConfigException $e) {
326
            Craft::error($e->getMessage(), __METHOD__);
327
        }
328
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
329
            '@nystudio107/seomatic/assetbundles/seomatic/dist',
330
            true
331
        );
332
        $variables['field'] = $this;
333
334
        // Render the settings template
335
        return Craft::$app->getView()->renderTemplate(
336
            'seomatic/_components/fields/SeoSettings_settings',
337
            $variables
338
        );
339
    }
340
341
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $value should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $element should have a doc-comment as per coding-style.
Loading history...
342
     * @inheritdoc
343
     */
344
    public function getInputHtml($value, ElementInterface $element = null): string
345
    {
346
        $variables = [];
347
        // JS/CSS modules
348
        try {
349
            Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
350
            ManifestHelper::registerCssModules([
351
                'styles.css',
352
                'vendors.css',
353
            ]);
354
            ManifestHelper::registerJsModules([
355
                'runtime.js',
356
                'vendors.js',
357
                'commons.js',
358
                'seomatic.js',
359
                'seomatic-tokens.js',
360
                'seomatic-meta.js',
361
            ]);
362
        } catch (InvalidConfigException $e) {
363
            Craft::error($e->getMessage(), __METHOD__);
364
        }
365
        // Asset bundle
366
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
367
            '@nystudio107/seomatic/assetbundles/seomatic/dist',
368
            true
369
        );
370
        // Basic variables
371
        $variables['name'] = $this->handle;
372
        $variables['value'] = $value;
373
        $variables['field'] = $this;
374
        $variables['currentSourceBundleType'] = 'entry';
375
        $variables['entitySchemaPath'] = SchemaHelper::getEntityPath($value->metaBundleSettings);
376
377
        // Get our id and namespace
378
        $id = Craft::$app->getView()->formatInputId($this->handle);
0 ignored issues
show
Bug introduced by
It seems like $this->handle can also be of type null; however, parameter $inputName of craft\web\View::formatInputId() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

378
        $id = Craft::$app->getView()->formatInputId(/** @scrutinizer ignore-type */ $this->handle);
Loading history...
379
        $nameSpacedId = Craft::$app->getView()->namespaceInputId($id);
380
        $variables['id'] = $id;
381
        $variables['nameSpacedId'] = $nameSpacedId;
382
        // Pull field sources
383
        if ($element !== null) {
384
            /** @var Element $element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
385
            $this->setContentFieldSourceVariables($element, 'Entry', $variables);
386
        }
387
388
        /** @var MetaBundle $value */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
389
        $variables['elementType'] = Asset::class;
390
        $variables['seoImageElements'] = ImageTransformHelper::assetElementsFromIds(
391
            $value->metaBundleSettings->seoImageIds,
392
            null
393
        );
394
        $variables['twitterImageElements'] = ImageTransformHelper::assetElementsFromIds(
395
            $value->metaBundleSettings->twitterImageIds,
396
            null
397
        );
398
        $variables['ogImageElements'] = ImageTransformHelper::assetElementsFromIds(
399
            $value->metaBundleSettings->ogImageIds,
400
            null
401
        );
402
        // Preview the containers so the preview is correct in the field
403
        if ($element !== null && $element->uri !== null) {
404
            Seomatic::$plugin->metaContainers->previewMetaContainers($element->uri, $element->siteId, true);
405
        }
406
407
        // Render the input template
408
        return Craft::$app->getView()->renderTemplate(
409
            'seomatic/_components/fields/SeoSettings_input',
410
            $variables
411
        );
412
    }
413
414
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $value should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $element should have a doc-comment as per coding-style.
Loading history...
415
     * @inheritdoc
416
     */
417
    public function getTableAttributeHtml($value, ElementInterface $element): string
418
    {
419
        $html = '';
420
        /** @var Element $element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
421
        if ($element !== null && $element->uri !== null) {
422
            $siteId = $element->siteId;
423
            $uri = $element->uri;
424
            $cacheKey = self::CACHE_KEY.$uri.$siteId.$this->elementDisplayPreviewType;
425
            $metaBundleSourceType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element);
426
            $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($metaBundleSourceType);
0 ignored issues
show
Bug introduced by
It seems like $metaBundleSourceType can also be of type null; however, parameter $metaBundleType of nystudio107\seomatic\ser...ementByMetaBundleType() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

426
            $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType(/** @scrutinizer ignore-type */ $metaBundleSourceType);
Loading history...
427
            $metaBundleSourceType = SeoEntry::getMetaBundleType();
428
            $metaBundleSourceId = '';
429
            if ($seoElement !== null) {
430
                $metaBundleSourceId = $seoElement::sourceIdFromElement($element);
431
            }
432
            $dependency = new TagDependency([
433
                'tags' => [
434
                    MetaContainers::GLOBAL_METACONTAINER_CACHE_TAG,
435
                    MetaContainers::METACONTAINER_CACHE_TAG.$metaBundleSourceId.$metaBundleSourceType.$siteId,
436
                    MetaContainers::METACONTAINER_CACHE_TAG.$uri.$siteId,
437
                ],
438
            ]);
439
            $cache = Craft::$app->getCache();
440
            $cacheDuration = null;
441
            $html = $cache->getOrSet(
442
                self::CACHE_KEY.$cacheKey,
443
                function () use ($uri, $siteId, $element) {
444
                    Seomatic::$plugin->metaContainers->previewMetaContainers($uri, $siteId, true);
445
                    $variables = [
446
                        'previewTypes' => [
447
                            $this->elementDisplayPreviewType ?? '',
448
                        ],
449
                        'previewElementId' => $element->id,
450
                    ];
451
                    // Render our preview table template
452
                    if (Seomatic::$matchedElement) {
453
                        return Craft::$app->getView()->renderTemplate(
454
                            'seomatic/_includes/table-preview.twig',
455
                            $variables
456
                        );
457
                    }
458
459
                    return '';
460
                },
461
                $cacheDuration,
462
                $dependency
463
            );
464
        }
465
466
        // Render the input template
467
        return $html;
468
    }
469
470
    // Protected Methods
471
    // =========================================================================
472
473
    /**
474
     * @param Element $element
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
475
     * @param string  $groupName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
476
     * @param array   $variables
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
477
     */
478
    protected function setContentFieldSourceVariables(
479
        Element $element,
480
        string $groupName,
481
        array &$variables
482
    ) {
483
        $variables['textFieldSources'] = array_merge(
484
            ['entryGroup' => ['optgroup' => $groupName.' Fields'], 'title' => 'Title'],
485
            FieldHelper::fieldsOfTypeFromElement(
486
                $element,
487
                FieldHelper::TEXT_FIELD_CLASS_KEY,
488
                false
489
            )
490
        );
491
        $variables['assetFieldSources'] = array_merge(
492
            ['entryGroup' => ['optgroup' => $groupName.' Fields']],
493
            FieldHelper::fieldsOfTypeFromElement(
494
                $element,
495
                FieldHelper::ASSET_FIELD_CLASS_KEY,
496
                false
497
            )
498
        );
499
        $variables['assetVolumeTextFieldSources'] = array_merge(
500
            ['entryGroup' => ['optgroup' => 'Asset Volume Fields'], 'title' => 'Title'],
501
            FieldHelper::fieldsOfTypeFromAssetVolumes(
502
                FieldHelper::TEXT_FIELD_CLASS_KEY,
503
                false
504
            )
505
        );
506
        $variables['userFieldSources'] = array_merge(
507
            ['entryGroup' => ['optgroup' => 'User Fields']],
508
            FieldHelper::fieldsOfTypeFromUsers(
509
                FieldHelper::TEXT_FIELD_CLASS_KEY,
510
                false
511
            )
512
        );
513
    }
514
}
515