MetaJsonLd::create()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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