Passed
Push — develop ( 498972...f9b89d )
by Andrew
08:33
created

SeoSettings::getContentColumnType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

288
                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...
289
                    if ($arrayValue !== null && \is_string($arrayValue)) {
290
                        $arrayValue = StringHelper::encodeMb4($arrayValue);
291
                    }
292
                });
293
            }
294
        }
295
296
        return $value;
297
    }
298
299
    /**
300
     * @inheritdoc
301
     */
302
    public function getSettingsHtml()
303
    {
304
        $variables = [];
305
        // JS/CSS modules
306
        try {
307
            Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
308
            $this->registerCssModules([
309
                'styles.css',
310
                'vendors.css',
311
            ]);
312
            $this->registerJsModules([
313
                'runtime.js',
314
                'vendors.js',
315
                'commons.js',
316
                'seomatic.js',
317
                'seomatic-tokens.js',
318
                'seomatic-meta.js',
319
            ]);
320
        } catch (InvalidConfigException $e) {
321
            Craft::error($e->getMessage(), __METHOD__);
322
        }
323
        // Asset bundle
324
        try {
325
            Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
326
        } catch (InvalidConfigException $e) {
327
            Craft::error($e->getMessage(), __METHOD__);
328
        }
329
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
330
            '@nystudio107/seomatic/assetbundles/seomatic/dist',
331
            true
332
        );
333
        $variables['field'] = $this;
334
335
        // Render the settings template
336
        return Craft::$app->getView()->renderTemplate(
337
            'seomatic/_components/fields/SeoSettings_settings',
338
            $variables
339
        );
340
    }
341
342
    /**
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...
343
     * @inheritdoc
344
     */
345
    public function getInputHtml($value, ElementInterface $element = null): string
346
    {
347
        $variables = [];
348
        // JS/CSS modules
349
        try {
350
            Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
351
            $this->registerCssModules([
352
                'styles.css',
353
                'vendors.css',
354
            ]);
355
            $this->registerJsModules([
356
                'runtime.js',
357
                'vendors.js',
358
                'commons.js',
359
                'seomatic.js',
360
                'seomatic-tokens.js',
361
                'seomatic-meta.js',
362
            ]);
363
        } catch (InvalidConfigException $e) {
364
            Craft::error($e->getMessage(), __METHOD__);
365
        }
366
        // Asset bundle
367
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
368
            '@nystudio107/seomatic/assetbundles/seomatic/dist',
369
            true
370
        );
371
        // Basic variables
372
        $variables['name'] = $this->handle;
373
        $variables['value'] = $value;
374
        $variables['field'] = $this;
375
        $variables['currentSourceBundleType'] = 'entry';
376
        $variables['entitySchemaPath'] = SchemaHelper::getEntityPath($value->metaBundleSettings);
377
378
        // Get our id and namespace
379
        $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

379
        $id = Craft::$app->getView()->formatInputId(/** @scrutinizer ignore-type */ $this->handle);
Loading history...
380
        $nameSpacedId = Craft::$app->getView()->namespaceInputId($id);
381
        $variables['id'] = $id;
382
        $variables['nameSpacedId'] = $nameSpacedId;
383
        // Pull field sources
384
        if ($element !== null) {
385
            /** @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...
386
            $this->setContentFieldSourceVariables($element, 'Entry', $variables);
387
        }
388
389
        /** @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...
390
        $variables['elementType'] = Asset::class;
391
        $variables['seoImageElements'] = ImageTransformHelper::assetElementsFromIds(
392
            $value->metaBundleSettings->seoImageIds,
393
            null
394
        );
395
        $variables['twitterImageElements'] = ImageTransformHelper::assetElementsFromIds(
396
            $value->metaBundleSettings->twitterImageIds,
397
            null
398
        );
399
        $variables['ogImageElements'] = ImageTransformHelper::assetElementsFromIds(
400
            $value->metaBundleSettings->ogImageIds,
401
            null
402
        );
403
        // Preview the containers so the preview is correct in the field
404
        if ($element !== null && $element->uri !== null) {
405
            Seomatic::$plugin->metaContainers->previewMetaContainers($element->uri, $element->siteId, true);
406
        }
407
408
        // Render the input template
409
        return Craft::$app->getView()->renderTemplate(
410
            'seomatic/_components/fields/SeoSettings_input',
411
            $variables
412
        );
413
    }
414
415
    /**
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...
416
     * @inheritdoc
417
     */
418
    public function getTableAttributeHtml($value, ElementInterface $element): string
419
    {
420
        $html = '';
421
        /** @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...
422
        if ($element !== null && $element->uri !== null) {
423
            $siteId = $element->siteId;
424
            $uri = $element->uri;
425
            $cacheKey = self::CACHE_KEY.$uri.$siteId.$this->elementDisplayPreviewType;
426
            $metaBundleSourceType = Seomatic::$plugin->seoElements->getMetaBundleTypeFromElement($element);
427
            $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

427
            $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType(/** @scrutinizer ignore-type */ $metaBundleSourceType);
Loading history...
428
            $metaBundleSourceType = SeoEntry::getMetaBundleType();
429
            $metaBundleSourceId = '';
430
            if ($seoElement !== null) {
431
                $metaBundleSourceId = $seoElement::sourceIdFromElement($element);
432
            }
433
            $dependency = new TagDependency([
434
                'tags' => [
435
                    MetaContainers::GLOBAL_METACONTAINER_CACHE_TAG,
436
                    MetaContainers::METACONTAINER_CACHE_TAG.$metaBundleSourceId.$metaBundleSourceType.$siteId,
437
                    MetaContainers::METACONTAINER_CACHE_TAG.$uri.$siteId,
438
                ],
439
            ]);
440
            $cache = Craft::$app->getCache();
441
            $cacheDuration = null;
442
            $html = $cache->getOrSet(
443
                self::CACHE_KEY.$cacheKey,
444
                function () use ($uri, $siteId, $element) {
445
                    Seomatic::$plugin->metaContainers->previewMetaContainers($uri, $siteId, true);
446
                    $variables = [
447
                        'previewTypes' => [
448
                            $this->elementDisplayPreviewType ?? '',
449
                        ],
450
                        'previewElementId' => $element->id,
451
                    ];
452
                    // Render our preview table template
453
                    if (Seomatic::$matchedElement) {
454
                        return Craft::$app->getView()->renderTemplate(
455
                            'seomatic/_includes/table-preview.twig',
456
                            $variables
457
                        );
458
                    }
459
460
                    return '';
461
                },
462
                $cacheDuration,
463
                $dependency
464
            );
465
        }
466
467
        // Render the input template
468
        return $html;
469
    }
470
471
    // Protected Methods
472
    // =========================================================================
473
474
    /**
475
     * @param Element $element
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
476
     * @param string  $groupName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
477
     * @param array   $variables
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
478
     */
479
    protected function setContentFieldSourceVariables(
480
        Element $element,
481
        string $groupName,
482
        array &$variables
483
    ) {
484
        $variables['textFieldSources'] = array_merge(
485
            ['entryGroup' => ['optgroup' => $groupName.' Fields'], 'title' => 'Title'],
486
            FieldHelper::fieldsOfTypeFromElement(
487
                $element,
488
                FieldHelper::TEXT_FIELD_CLASS_KEY,
489
                false
490
            )
491
        );
492
        $variables['assetFieldSources'] = array_merge(
493
            ['entryGroup' => ['optgroup' => $groupName.' Fields']],
494
            FieldHelper::fieldsOfTypeFromElement(
495
                $element,
496
                FieldHelper::ASSET_FIELD_CLASS_KEY,
497
                false
498
            )
499
        );
500
        $variables['assetVolumeTextFieldSources'] = array_merge(
501
            ['entryGroup' => ['optgroup' => 'Asset Volume Fields'], 'title' => 'Title'],
502
            FieldHelper::fieldsOfTypeFromAssetVolumes(
503
                FieldHelper::TEXT_FIELD_CLASS_KEY,
504
                false
505
            )
506
        );
507
        $variables['userFieldSources'] = array_merge(
508
            ['entryGroup' => ['optgroup' => 'User Fields']],
509
            FieldHelper::fieldsOfTypeFromUsers(
510
                FieldHelper::TEXT_FIELD_CLASS_KEY,
511
                false
512
            )
513
        );
514
    }
515
516
    /**
517
     * Register CSS modules so they can be run through webpack-dev-server
518
     *
519
     * @param array $modules
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
520
     */
521
    protected function registerCssModules(array $modules)
522
    {
523
        foreach ($modules as $moduleName) {
524
            try {
525
                $module = Seomatic::$seomaticVariable->manifest->getModuleUri($moduleName);
526
            } catch (NotFoundHttpException $e) {
527
                Craft::error($e->getMessage(), __METHOD__);
528
                $module = null;
529
            }
530
            if ($module) {
531
                Seomatic::$view->registerCssFile($module, [
532
                    'depends' => CpAsset::class,
533
                ]);
534
            }
535
        }
536
    }
537
538
    /**
539
     * Register JavaScript modules so they can be run through webpack-dev-server
540
     *
541
     * @param array $modules
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
542
     */
543
    protected function registerJsModules(array $modules)
544
    {
545
        foreach ($modules as $moduleName) {
546
            try {
547
                $module = Seomatic::$seomaticVariable->manifest->getModuleUri($moduleName);
548
            } catch (NotFoundHttpException $e) {
549
                Craft::error($e->getMessage(), __METHOD__);
550
                $module = null;
551
            }
552
            if ($module) {
553
                Seomatic::$view->registerJsFile($module, [
554
                    'depends' => CpAsset::class,
555
                ]);
556
            }
557
        }
558
    }
559
}
560