Passed
Push — v3 ( 5141f6...ef9227 )
by Andrew
31:01 queued 18:03
created

MetaJsonLd::renderAttributes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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