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

MetaJsonLd   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 467
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 61
eloc 172
c 2
b 0
f 0
dl 0
loc 467
ccs 0
cts 180
cp 0
rs 3.52

16 Methods

Rating   Name   Duplication   Size   Complexity  
A datetimeAttributes() 0 3 1
A getSchemaPropertyExpectedTypes() 0 3 1
B render() 0 53 9
A getSchemaPropertyNames() 0 3 1
A create() 0 22 3
A __toString() 0 6 1
A getGoogleRecommendedSchema() 0 3 1
A init() 0 8 2
A renderAttributes() 0 15 2
A fields() 0 10 3
A generateNonce() 0 10 2
F validateJsonSchema() 0 113 30
A prepForRender() 0 7 2
A getSchemaPropertyDescriptions() 0 3 1
A rules() 0 15 1
A getGoogleRequiredSchema() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like MetaJsonLd 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 MetaJsonLd, and based on these observations, apply Extract Interface, too.

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\models;
13
14
use Craft;
15
use craft\helpers\Json;
16
use craft\helpers\Template;
17
use craft\validators\UrlValidator;
18
use DateTime;
19
use Exception;
20
use nystudio107\seomatic\base\NonceItem;
21
use nystudio107\seomatic\helpers\JsonLd as JsonLdHelper;
22
use nystudio107\seomatic\Seomatic;
23
use yii\validators\BooleanValidator;
24
use yii\validators\DateValidator;
25
use yii\validators\NumberValidator;
26
use function in_array;
27
use function is_array;
28
use function is_object;
29
30
/**
31
 * @author    nystudio107
32
 * @package   Seomatic
33
 * @since     3.0.0
34
 */
35
class MetaJsonLd extends NonceItem
36
{
37
    // Constants
38
    // =========================================================================
39
40
    public const ITEM_TYPE = 'MetaJsonLd';
41
42
    protected const SCHEMA_NAMESPACE_PREFIX = 'nystudio107\\seomatic\\models\\jsonld\\';
43
    protected const SCHEMA_NAME_PREFIX = 'Schema_';
44
45
    // Static Properties
46
    // =========================================================================
47
48
    /**
49
     * The Schema.org Type Name
50
     *
51
     * @var string
52
     */
53
    static public string $schemaTypeName = 'JsonLd';
54
55
    /**
56
     * The Schema.org Type Scope
57
     *
58
     * @var string
59
     */
60
    static public string $schemaTypeScope = 'https://schema.org/';
61
62
    /**
63
     * The Schema.org Type Description
64
     *
65
     * @var string
66
     */
67
    static public string $schemaTypeDescription = 'Generic JsonLd type.';
68
69
    /**
70
     * The Schema.org Type Extends
71
     *
72
     * @var string
73
     */
74
    static public string $schemaTypeExtends = '';
75
76
    // Public Properties
77
    // =========================================================================
78
79
    /**
80
     * The schema context.
81
     *
82
     * @var string [schema.org types: Text]
83
     */
84
    public $context;
85
86
    /**
87
     * The item's type.
88
     *
89
     * @var string [schema.org types: Text]
90
     */
91
    public $type;
92
93
    /**
94
     * The item's id.
95
     *
96
     * @var string [schema.org types: Text]
97
     */
98
    public $id;
99
100
    /**
101
     * The JSON-LD graph https://json-ld.org/spec/latest/json-ld/#named-graphs
102
     *
103
     * @var null|array
104
     */
105
    public $graph;
106
107
108
    // Static Methods
109
    // =========================================================================
110
111
    /**
112
     * Create a new JSON-LD schema type object
113
     *
114
     * @param string $schemaType
115
     * @param array $config
116
     *
117
     * @return MetaJsonLd
118
     */
119
    public static function create($schemaType, array $config = []): MetaJsonLd
120
    {
121
        // Try the passed in $schemaType
122
        $className = self::SCHEMA_NAMESPACE_PREFIX . $schemaType;
123
        /** @var $model MetaJsonLd */
124
        if (class_exists($className)) {
125
            self::cleanProperties($className, $config);
126
127
            return new $className($config);
128
        }
129
        // Try the prefixed $schemaType
130
        $className = self::SCHEMA_NAMESPACE_PREFIX . self::SCHEMA_NAME_PREFIX . $schemaType;
131
        /** @var $model MetaJsonLd */
132
        if (class_exists($className)) {
133
            self::cleanProperties($className, $config);
134
135
            return new $className($config);
136
        }
137
        // Fall back on returning a generic schema.org type
138
        self::cleanProperties(__CLASS__, $config);
139
140
        return new MetaJsonLd($config);
141
    }
142
143
    // Public Methods
144
    // =========================================================================
145
146
    /**
147
     * Set the $type & $context properties
148
     *
149
     * @inheritdoc
150
     */
151
    public function init(): void
152
    {
153
        parent::init();
154
155
        $this->type = static::$schemaTypeName;
156
        $this->context = 'http://schema.org';
157
        // Make sure we have a valid key
158
        $this->key = $this->key ?: lcfirst($this->type);
159
    }
160
161
    /**
162
     * Renders a JSON-LD representation of the schema
163
     *
164
     * @return string The resulting JSON-LD
165
     */
166
    public function __toString()
167
    {
168
        return $this->render([
169
            'renderRaw' => false,
170
            'renderScriptTags' => true,
171
            'array' => false,
172
        ]);
173
    }
174
175
    /**
176
     * Return the Schema.org Property Names
177
     *
178
     * @return array
179
     */
180
    public function getSchemaPropertyNames(): array
181
    {
182
        return array_keys($this->getSchemaPropertyExpectedTypes());
183
    }
184
185
186
    /**
187
     * Return the Schema.org Property Expected Types
188
     *
189
     * @return array
190
     */
191
    public function getSchemaPropertyExpectedTypes(): array
192
    {
193
        return [];
194
    }
195
196
    /**
197
     * Return the Schema.org Property Descriptions
198
     *
199
     * @return array
200
     */
201
    public function getSchemaPropertyDescriptions(): array
202
    {
203
        return [];
204
    }
205
206
    /**
207
     * Return the Schema.org Google Required Schema for this type
208
     *
209
     * @return array
210
     */
211
    public function getGoogleRequiredSchema(): array
212
    {
213
        return [];
214
    }
215
216
    /**
217
     * Return the Schema.org Google Recommended Schema for this type
218
     *
219
     * @return array
220
     */
221
    public function getGoogleRecommendedSchema(): array
222
    {
223
        return [];
224
    }
225
226
    /**
227
     * @inheritdoc
228
     */
229
    public function prepForRender(&$data): bool
230
    {
231
        $shouldRender = parent::prepForRender($data);
232
        if ($shouldRender) {
233
        }
234
235
        return $shouldRender;
236
    }
237
238
    /**
239
     * Renders a JSON-LD representation of the schema
240
     *
241
     * @param array $params
242
     *
243
     * @return string
244
     */
245
    public function render(
246
        array $params = [
247
            'renderRaw' => true,
248
            'renderScriptTags' => true,
249
            'array' => false,
250
        ]
251
    ): string
252
    {
253
        $html = '';
254
        $options = $this->tagAttributes();
255
        if ($this->prepForRender($options)) {
256
            $linebreak = '';
257
            // If we're rendering for an array, don't add linebreaks
258
            $oldDevMode = Seomatic::$devMode;
259
            if ($params['array'] === true) {
260
                Seomatic::$devMode = false;
261
            }
262
            // If `devMode` is enabled, make the JSON-LD human-readable
263
            if (Seomatic::$devMode) {
264
                $linebreak = PHP_EOL;
265
            }
266
            // Render the resulting JSON-LD
267
            $scenario = $this->scenario;
268
            $this->setScenario('render');
269
            try {
270
                $html = JsonLdHelper::encode($this);
271
            } catch (Exception $e) {
272
                Craft::error($e, __METHOD__);
273
                Craft::$app->getErrorHandler()->logException($e);
274
            }
275
            $this->setScenario($scenario);
276
            if ($params['array'] === true) {
277
                Seomatic::$devMode = $oldDevMode;
278
            }
279
            if ($params['renderScriptTags']) {
280
                $html =
281
                    '<script type="application/ld+json">'
282
                    . $linebreak
283
                    . $html
284
                    . $linebreak
285
                    . '</script>';
286
            } elseif (Seomatic::$devMode) {
287
                $html =
288
                    $linebreak
289
                    . $html
290
                    . $linebreak;
291
            }
292
            if ($params['renderRaw'] === true) {
293
                $html = Template::raw($html);
294
            }
295
        }
296
297
        return $html;
298
    }
299
300
    /**
301
     * @inheritdoc
302
     */
303
    public function renderAttributes(array $params = []): array
304
    {
305
        $attributes = [];
306
307
        $result = Json::decodeIfJson($this->render([
308
            'renderRaw' => true,
309
            'renderScriptTags' => false,
310
            'array' => true,
311
312
        ]));
313
        if ($result !== false) {
314
            $attributes = $result;
315
        }
316
317
        return $attributes;
318
    }
319
320
    /**
321
     * @inheritdoc
322
     */
323
    public function fields(): array
324
    {
325
        $fields = parent::fields();
326
        switch ($this->scenario) {
327
            case 'google':
328
            case 'default':
329
                break;
330
        }
331
332
        return $fields;
333
    }
334
335
    /**
336
     * @inheritdoc
337
     */
338
    public function rules(): array
339
    {
340
        $rules = parent::rules();
341
        $rules = array_merge($rules, [
342
            [
343
                [
344
                    'id',
345
                    'type',
346
                    'context',
347
                ],
348
                'string',
349
            ],
350
        ]);
351
352
        return $rules;
353
    }
354
355
    /**
356
     * We don't want Craft's base Model messing with our dateCreated etc properties
357
     *
358
     * @return array|string[]
359
     */
360
    public function datetimeAttributes(): array
361
    {
362
        return [];
363
    }
364
365
    /**
366
     * Validate the passed in $attribute based on $schemaPropertyExpectedTypes
367
     *
368
     * @param string $attribute the attribute currently being validated
369
     * @param mixed $params the value of the "params" given in the rule
370
     */
371
    public function validateJsonSchema(
372
        $attribute,
373
        $params
374
    )
375
    {
376
        if (!in_array($attribute, $this->getSchemaPropertyNames(), true)) {
377
            $this->addError($attribute, 'The attribute does not exist.');
378
        } else {
379
            $expectedTypes = $this->getSchemaPropertyExpectedTypes()[$attribute];
380
            $validated = false;
381
            $dataToValidate = $this->$attribute;
382
            if (!is_array($dataToValidate)) {
383
                $dataToValidate = [$dataToValidate];
384
            }
385
            foreach ($dataToValidate as $key => $data) {
386
                /** @var array $expectedTypes */
387
                foreach ($expectedTypes as $expectedType) {
388
                    $className = 'nystudio107\\seomatic\\models\\jsonld\\' . $expectedType;
389
                    switch ($expectedType) {
390
                        // Text always validates
391
                        case 'Text':
392
                            $validated = true;
393
                            break;
394
395
                        // Use Yii's validator for URLs
396
                        case 'URL':
397
                            $validator = new UrlValidator;
398
                            if ($validator->validate($data, $error)) {
399
                                $validated = true;
400
                            }
401
                            break;
402
403
                        // Use Yii's validator for Booleans
404
                        case 'Boolean':
405
                            $validator = new BooleanValidator;
406
                            if ($validator->validate($data, $error)) {
407
                                $validated = true;
408
                            }
409
                            break;
410
411
                        // Use Yii's validator for Numbers
412
                        case 'Number':
413
                        case 'Float':
414
                        case 'Integer':
415
                            $validator = new NumberValidator;
416
                            if ($expectedType === 'Integer') {
417
                                $validator->integerOnly = true;
418
                            }
419
                            if ($validator->validate($data, $error)) {
420
                                $validated = true;
421
                            }
422
                            break;
423
424
                        // Use Yii's validator for Dates
425
                        case 'Date':
426
                            $validator = new DateValidator;
427
                            $validator->type = DateValidator::TYPE_DATE;
428
                            $validator->format = 'php:' . DateTime::ATOM;
429
                            if ($validator->validate($data, $error)) {
430
                                $validated = true;
431
                            }
432
                            $validator->format = 'YYYY-MM-DD';
433
                            if ($validator->validate($data, $error)) {
434
                                $validated = true;
435
                            }
436
                            break;
437
438
                        // Use Yii's validator for DateTimes
439
                        case 'DateTime':
440
                            $validator = new DateValidator;
441
                            $validator->type = DateValidator::TYPE_DATETIME;
442
                            $validator->format = 'YYYY-MM-DDThh:mm:ss.sTZD';
443
                            if ($validator->validate($data, $error)) {
444
                                $validated = true;
445
                            }
446
                            break;
447
448
                        // Use Yii's validator for Times
449
                        case 'Time':
450
                            $validator = new DateValidator;
451
                            $validator->type = DateValidator::TYPE_TIME;
452
                            $validator->format = 'hh:mm:ss.sTZD';
453
                            if ($validator->validate($data, $error)) {
454
                                $validated = true;
455
                            }
456
                            break;
457
                        // Match an ISO 8601 duration as per: https://stackoverflow.com/questions/32044846/regex-for-iso-8601-durations
458
                        case 'Duration':
459
                            if (preg_match('/^P(?!$)((\d+Y)|(\d+\.\d+Y$))?((\d+M)|(\d+\.\d+M$))?((\d+W)|(\d+\.\d+W$))?((\d+D)|(\d+\.\d+D$))?(T(?=\d)((\d+H)|(\d+\.\d+H$))?((\d+M)|(\d+\.\d+M$))?(\d+(\.\d+)?S)?)??$/', $data) === 1) {
460
                                $validated = true;
461
                            }
462
                            break;
463
464
                        // By default, assume it's a schema.org JSON-LD object, and validate that
465
                        default:
466
                            // Allow for @id references
467
                            if ($key === 'id') {
468
                                $validated = true;
469
                            }
470
                            if (is_object($data) && ($data instanceof $className)) {
471
                                $validated = true;
472
                            }
473
                            if (is_string($data)) {
474
                                $targetClass = 'nystudio107\\seomatic\\models\\jsonld\\' . $data;
475
                                if (class_exists($targetClass)) {
476
                                    $validated = true;
477
                                }
478
                            }
479
                            break;
480
                    }
481
                }
482
                if (!$validated) {
483
                    $this->addError($attribute, 'Must be one of these types: ' . implode(', ', $expectedTypes));
484
                }
485
            }
486
        }
487
    }
488
489
    /**
490
     * @inheritDoc
491
     */
492
    public function generateNonce()
493
    {
494
        $result = parent::generateNonce();
495
        if (Seomatic::$plugin->metaContainers->cachedJsonLdNonce !== null) {
496
            return Seomatic::$plugin->metaContainers->cachedJsonLdNonce;
497
        }
498
499
        Seomatic::$plugin->metaContainers->cachedJsonLdNonce = $result;
500
501
        return $result;
502
    }
503
504
// Private Methods
505
// =========================================================================
506
}
507