Passed
Push — v3 ( 1e51d3...178405 )
by Andrew
25:46
created

src/helpers/Schema.php (1 issue)

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\helpers;
13
14
use nystudio107\seomatic\models\MetaJsonLd;
15
16
use Craft;
17
use craft\helpers\Json as JsonHelper;
18
19
/**
20
 * @author    nystudio107
21
 * @package   Seomatic
22
 * @since     3.0.0
23
 */
24
class Schema
25
{
26
    // Constants
27
    // =========================================================================
28
29
    const SCHEMA_PATH_DELIMITER = '.';
30
    const MENU_INDENT_STEP = 4;
31
32
    const SCHEMA_TYPES = [
33
        'siteSpecificType',
34
        'siteSubType',
35
        'siteType',
36
    ];
37
38
    // Static Properties
39
    // =========================================================================
40
41
    protected static $schemaTypes = [];
42
43
    protected static $schemaTree = [];
44
45
    // Static Methods
46
    // =========================================================================
47
48
    /**
49
     * Return the most specific schema.org type possible from the $settings
50
     *
51
     * @param $settings
52
     *
53
     * @param bool $allowEmpty
54
     * @return string
55
     */
56
    public static function getSpecificEntityType($settings, bool $allowEmpty = false): string
57
    {
58
        if (!empty($settings)) {
59
            // Go from most specific type to least specific type
60
            foreach (self::SCHEMA_TYPES as $schemaType) {
61
                if (!empty($settings[$schemaType]) && ($settings[$schemaType] !== 'none')) {
62
                    return $settings[$schemaType];
63
                }
64
            }
65
        }
66
67
        return $allowEmpty ? '' : 'WebPage';
68
    }
69
70
    /**
71
     * Return a period-delimited schema.org path from the $settings
72
     *
73
     * @param $settings
74
     *
75
     * @return string
76
     */
77
    public static function getEntityPath($settings): string
78
    {
79
        $result = '';
80
        if (!empty($settings)) {
81
            // Go from most specific type to least specific type
82
            foreach (self::SCHEMA_TYPES as $schemaType) {
83
                if (!empty($settings[$schemaType]) && ($settings[$schemaType] !== 'none')) {
84
                    $result = $settings[$schemaType].self::SCHEMA_PATH_DELIMITER.$result;
85
                }
86
            }
87
        }
88
89
        return rtrim($result, self::SCHEMA_PATH_DELIMITER);
90
    }
91
92
    /**
93
     * Get the fully composed schema type
94
     *
95
     * @param $schemaType
96
     *
97
     * @return array
98
     */
99
    public static function getSchemaType(string $schemaType): array
100
    {
101
        $result = [];
102
        $jsonLdType = MetaJsonLd::create($schemaType);
103
104
        if ($jsonLdType) {
0 ignored issues
show
$jsonLdType is of type nystudio107\seomatic\models\MetaJsonLd, thus it always evaluated to true.
Loading history...
105
            // Get the static properties
106
            try {
107
                $classRef = new \ReflectionClass(\get_class($jsonLdType));
108
            } catch (\ReflectionException $e) {
109
                $classRef = null;
110
            }
111
            if ($classRef) {
112
                $result = $classRef->getStaticProperties();
113
            }
114
        }
115
116
        return $result;
117
    }
118
119
    /**
120
     * Get the decomposed schema type
121
     *
122
     * @param string $schemaType
123
     *
124
     * @return array
125
     */
126
    public static function getDecomposedSchemaType(string $schemaType): array
127
    {
128
        $result = [];
129
        while ($schemaType) {
130
            $className = 'nystudio107\\seomatic\\models\\jsonld\\'.$schemaType;
131
            if (class_exists($className)) {
132
                try {
133
                    $classRef = new \ReflectionClass($className);
134
                } catch (\ReflectionException $e) {
135
                    $classRef = null;
136
                }
137
                if ($classRef) {
138
                    $staticProps = $classRef->getStaticProperties();
139
140
                    foreach ($staticProps as $key => $value) {
141
                        if ($key[0] === '_') {
142
                            $newKey = ltrim($key, '_');
143
                            $staticProps[$newKey] = $value;
144
                            unset($staticProps[$key]);
145
                        }
146
                    }
147
                    $result[$schemaType] = $staticProps;
148
                    $schemaType = $staticProps['schemaTypeExtends'];
149
                    if ($schemaType === 'JsonLdType') {
150
                        $schemaType = null;
151
                    }
152
                }
153
            }
154
        }
155
156
        return $result;
157
    }
158
159
    /**
160
     * Return a flattened, indented menu of the given $path
161
     *
162
     * @param string $path
163
     *
164
     * @return array
165
     */
166
    public static function getTypeMenu($path = ''): array
167
    {
168
        try {
169
            $schemaTypes = self::getSchemaArray($path);
170
        } catch (\Exception $e) {
171
            Craft::error($e->getMessage(), __METHOD__);
172
            return [];
173
        }
174
175
        return self::flattenSchemaArray($schemaTypes, 0);
176
    }
177
178
    /**
179
     * Return a single menu of schemas starting at $path
180
     *
181
     * @param string $path
182
     *
183
     * @return array
184
     */
185
    public static function getSingleTypeMenu($path = ''): array
186
    {
187
        $result = [];
188
        try {
189
            $schemaTypes = self::getSchemaArray($path);
190
        } catch (\Exception $e) {
191
            Craft::error($e->getMessage(), __METHOD__);
192
            return [];
193
        }
194
        foreach ($schemaTypes as $key => $value) {
195
            $result[$key] = $key;
196
        }
197
198
        return $result;
199
    }
200
201
    /**
202
     * Return a flattened, indented menu of the given $path
203
     *
204
     * @param string $path
205
     *
206
     * @return array
207
     */
208
    public static function getTypeTree(): array
209
    {
210
        try {
211
            $schemaTypes = self::getSchemaTree();
212
        } catch (\Exception $e) {
213
            Craft::error($e->getMessage(), __METHOD__);
214
            return [];
215
        }
216
        $schemaTypes = self::pruneSchemaTree($schemaTypes, '');
217
218
        // Ignore the top level "Thing" base schema
219
        return $schemaTypes['children'] ?? [];
220
    }
221
222
    /**
223
     * Return a hierarchical array of schema types, starting at $path. The $path
224
     * is specified as SchemaType.SubSchemaType using SCHEMA_PATH_DELIMITER as
225
     * the delimiter.
226
     *
227
     * @param string $path
228
     *
229
     * @return array
230
     * @throws \Exception
231
     */
232
    public static function getSchemaArray($path = ''): array
233
    {
234
        if (empty(self::$schemaTypes)) {
235
            $filePath = Craft::getAlias('@nystudio107/seomatic/resources/schema/tree.jsonld');
236
            self::$schemaTypes = JsonHelper::decode(@file_get_contents($filePath));
237
            if (empty(self::$schemaTypes)) {
238
                throw new \Exception(Craft::t('seomatic', 'Schema tree file not found'));
239
            }
240
            self::$schemaTypes = self::makeSchemaAssociative(self::$schemaTypes);
241
            self::$schemaTypes = self::orphanChildren(self::$schemaTypes);
242
        }
243
        // Get just the appropriate sub-array
244
        $typesArray = self::$schemaTypes;
245
        if (!empty($path)) {
246
            $keys = explode(self::SCHEMA_PATH_DELIMITER, $path);
247
            foreach ($keys as $key) {
248
                if (!empty($typesArray[$key])) {
249
                    $typesArray = $typesArray[$key];
250
                }
251
            }
252
        }
253
        if (!\is_array($typesArray)) {
254
            $typesArray = [];
255
        }
256
257
        return $typesArray;
258
    }
259
260
    /**
261
     * @return array
262
     * @throws \Exception
263
     */
264
    public static function getSchemaTree()
265
    {
266
        if (empty(self::$schemaTree)) {
267
            $filePath = Craft::getAlias('@nystudio107/seomatic/resources/schema/tree.jsonld');
268
            self::$schemaTree = JsonHelper::decode(@file_get_contents($filePath));
269
            if (empty(self::$schemaTree)) {
270
                throw new \Exception(Craft::t('seomatic', 'Schema tree file not found'));
271
            }
272
        }
273
        $typesArray = self::$schemaTree;
274
        if (!\is_array($typesArray)) {
275
            $typesArray = [];
276
        }
277
278
        return $typesArray;
279
    }
280
281
    /**
282
     * @param array $typesArray
283
     * @param       $indentLevel
284
     *
285
     * @return array
286
     */
287
    protected static function flattenSchemaArray(array $typesArray, int $indentLevel): array
288
    {
289
        $result = [];
290
        foreach ($typesArray as $key => $value) {
291
            $indent = html_entity_decode(str_repeat('&nbsp;', $indentLevel));
292
            if (\is_array($value)) {
293
                $result[$key] = $indent . $key;
294
                $value = self::flattenSchemaArray($value, $indentLevel + self::MENU_INDENT_STEP);
295
                $result = array_merge($result, $value);
296
            } else {
297
                $result[$key] = $indent . $value;
298
            }
299
        }
300
301
        return $result;
302
    }
303
304
    /**
305
     * Reduce everything in the $schemaTypes array to a simple hierarchical
306
     * array as 'SchemaType' => 'SchemaType' if it has no children, and if it
307
     * has children, as 'SchemaType' = [] with an array of sub-types
308
     *
309
     * @param array $typesArray
310
     *
311
     * @return array
312
     */
313
    protected static function orphanChildren(array $typesArray): array
314
    {
315
        $result = [];
316
317
        if (!empty($typesArray['children']) && \is_array($typesArray['children'])) {
318
            foreach ($typesArray['children'] as $key => $value) {
319
                $key = '';
320
                if (!empty($value['name'])) {
321
                    $key = $value['name'];
322
                }
323
                if (!empty($value['children'])) {
324
                    $value = self::orphanChildren($value);
325
                } else {
326
                    $value = $key;
327
                }
328
                if (!empty($key)) {
329
                    $result[$key] = $value;
330
                }
331
            }
332
        }
333
334
        return $result;
335
    }
336
337
    /**
338
     * Return a new array that has each type returned as an associative array
339
     * as 'SchemaType' => [] rather than the way the tree.jsonld file has it
340
     * stored as a non-associative array
341
     *
342
     * @param array $typesArray
343
     *
344
     * @return array
345
     */
346
    protected static function makeSchemaAssociative(array $typesArray): array
347
    {
348
        $result = [];
349
350
        foreach ($typesArray as $key => $value) {
351
            if (isset($value['name'])) {
352
                $key = $value['name'];
353
            }
354
            if (\is_array($value)) {
355
                $value = self::makeSchemaAssociative($value);
356
            }
357
            if (isset($value['layer']) && \is_string($value['layer'])) {
358
                if ($value['layer'] === 'core' || $value['layer'] === 'pending') {
359
                    $result[$key] = $value;
360
                }
361
            } else {
362
                $result[$key] = $value;
363
            }
364
        }
365
366
        return $result;
367
    }
368
369
    /**
370
     * Prune the schema tree by removing everything but `label`, `id`, and `children`
371
     * in preparation for use by the treeselect component. Also make the id a namespaced
372
     * path to the schema, with the first two higher level schema types, and then the
373
     * third final type (skipping any in between)
374
     *
375
     * @param array $typesArray
376
     * @param string $path
377
     * @return array
378
     */
379
    protected static function pruneSchemaTree(array $typesArray, string $path): array
380
    {
381
        $result = [];
382
383
        if (isset($typesArray['layer']) && \is_string($typesArray['layer'])) {
384
            if ($typesArray['layer'] === 'core' || $typesArray['layer'] === 'pending') {
385
                if (isset($typesArray['name']) && \is_string($typesArray['name'])) {
386
                    $children = [];
387
                    $name = $typesArray['name'];
388
                    // Construct a path-based $id, excluding the top-level `Thing` schema
389
                    $id = $name === 'Thing' ? '' : $path.self::SCHEMA_PATH_DELIMITER.$name;
390
                    $id = ltrim($id, self::SCHEMA_PATH_DELIMITER);
391
                    // Make sure we have at most 3 specifiers in the schema path
392
                    $parts = explode(self::SCHEMA_PATH_DELIMITER, $id);
393
                    if (count($parts) > 3) {
394
                        $id = implode(self::SCHEMA_PATH_DELIMITER, [
395
                            $parts[0],
396
                            $parts[1],
397
                            end($parts)
398
                        ]);
399
                    }
400
                    if (!empty($typesArray['children'])) {
401
                        foreach ($typesArray['children'] as $child) {
402
                            $childResult = self::pruneSchemaTree($child, $id);
403
                            if (!empty($childResult)) {
404
                                $children[] = $childResult;
405
                            }
406
                        }
407
                        $result['children'] = $children;
408
                    }
409
                    $result['label'] = $name;
410
                    $result['id'] = $id;
411
                }
412
            }
413
        }
414
415
        return $result;
416
    }
417
}
418