MetaJsonLd::validateJsonSchema()   F
last analyzed

Complexity

Conditions 30
Paths 163

Size

Total Lines 112
Code Lines 74

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 74
dl 0
loc 112
rs 3.6416
c 0
b 0
f 0
cc 30
nc 163
nop 2

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