Completed
Push — feature/EVO-3239-migration-for... ( 6f095c )
by Lucas
10:27
created

SchemaUtils::getSchemaRouteName()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3
Metric Value
dl 0
loc 13
ccs 8
cts 8
cp 1
rs 9.4285
cc 3
eloc 7
nc 2
nop 1
crap 3
1
<?php
2
/**
3
 * Utils for generating schemas.
4
 */
5
6
namespace Graviton\SchemaBundle;
7
8
use Graviton\I18nBundle\Document\TranslatableDocumentInterface;
9
use Graviton\I18nBundle\Document\Language;
10
use Graviton\I18nBundle\Repository\LanguageRepository;
11
use Graviton\RestBundle\Model\DocumentModel;
12
use Graviton\SchemaBundle\Document\Schema;
13
use Graviton\SchemaBundle\Service\RepositoryFactory;
14
use Metadata\MetadataFactoryInterface as SerializerMetadataFactoryInterface;
15
use Symfony\Component\Routing\RouterInterface;
16
17
/**
18
 * Utils for generating schemas.
19
 *
20
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
21
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
22
 * @link     http://swisscom.ch
23
 */
24
class SchemaUtils
25
{
26
27
    /**
28
     * language repository
29
     *
30
     * @var LanguageRepository repository
31
     */
32
    private $languageRepository;
33
34
    /**
35
     * router
36
     *
37
     * @var RouterInterface router
38
     */
39
    private $router;
40
41
    /**
42
     * mapping service names => route names
43
     *
44
     * @var array service mapping
45
     */
46
    private $extrefServiceMapping;
47
48
    /**
49
     * event map
50
     *
51
     * @var array event map
52
     */
53
    private $eventMap;
54
55
    /**
56
     * @var array [document class => [field name -> exposed name]]
57
     */
58
    private $documentFieldNames;
59
60
    /**
61
     * @var string
62
     */
63
    private $defaultLocale;
64
65
    /**
66
     * @var RepositoryFactory
67
     */
68
    private $repositoryFactory;
69
70
    /**
71
     * @var SerializerMetadataFactoryInterface
72
     */
73
    private $serializerMetadataFactory;
74
75
    /**
76
     * Constructor
77
     *
78
     * @param RepositoryFactory                  $repositoryFactory         Create repos from model class names
79
     * @param SerializerMetadataFactoryInterface $serializerMetadataFactory Serializer metadata factory
80
     * @param LanguageRepository                 $languageRepository        repository
81
     * @param RouterInterface                    $router                    router
82
     * @param array                              $extrefServiceMapping      Extref service mapping
83
     * @param array                              $eventMap                  eventmap
84
     * @param array                              $documentFieldNames        Document field names
85
     * @param string                             $defaultLocale             Default Language
86
     */
87 2
    public function __construct(
88
        RepositoryFactory $repositoryFactory,
89
        SerializerMetadataFactoryInterface $serializerMetadataFactory,
90
        LanguageRepository $languageRepository,
91
        RouterInterface $router,
92
        array $extrefServiceMapping,
93
        array $eventMap,
94
        array $documentFieldNames,
95
        $defaultLocale
96
    ) {
97 2
        $this->repositoryFactory = $repositoryFactory;
98 2
        $this->serializerMetadataFactory = $serializerMetadataFactory;
99 2
        $this->languageRepository = $languageRepository;
100 2
        $this->router = $router;
101 2
        $this->extrefServiceMapping = $extrefServiceMapping;
102 2
        $this->eventMap = $eventMap;
103 2
        $this->documentFieldNames = $documentFieldNames;
104 2
        $this->defaultLocale = $defaultLocale;
105 2
    }
106
107
    /**
108
     * get schema for an array of models
109
     *
110
     * @param string        $modelName name of model
111
     * @param DocumentModel $model     model
112
     *
113
     * @return Schema
114
     */
115
    public function getCollectionSchema($modelName, DocumentModel $model)
116
    {
117
        $collectionSchema = new Schema;
118
        $collectionSchema->setTitle(sprintf('Array of %s objects', $modelName));
119
        $collectionSchema->setType('array');
120
        $collectionSchema->setItems($this->getModelSchema($modelName, $model));
121
122
        return $collectionSchema;
123
    }
124
125
    /**
126
     * return the schema for a given route
127
     *
128
     * @param string        $modelName name of mode to generate schema for
129
     * @param DocumentModel $model     model to generate schema for
130
     * @param boolean       $online    if we are online and have access to mongodb during this build
131
     *
132
     * @return Schema
133
     */
134
    public function getModelSchema($modelName, DocumentModel $model, $online = true)
135
    {
136
        // build up schema data
137
        $schema = new Schema;
138
139
        if (!empty($model->getTitle())) {
140
            $schema->setTitle($model->getTitle());
141
        } else {
142
            $schema->setTitle(ucfirst($modelName));
143
        }
144
        
145
        $schema->setDescription($model->getDescription());
146
        $schema->setType('object');
147
148
        // grab schema info from model
149
        $repo = $model->getRepository();
150
        $meta = $repo->getClassMetadata();
151
152
        // look for translatables in document class
153
        $documentReflection = new \ReflectionClass($repo->getClassName());
154
        if ($documentReflection->implementsInterface('Graviton\I18nBundle\Document\TranslatableDocumentInterface')) {
155
            /** @var TranslatableDocumentInterface $documentInstance */
156
            $documentInstance = $documentReflection->newInstanceWithoutConstructor();
157
            $translatableFields = array_merge(
158
                $documentInstance->getTranslatableFields(),
159
                $documentInstance->getPreTranslatedFields()
160
            );
161
        } else {
162
            $translatableFields = [];
163
        }
164
165
        // exposed fields
166
        $documentFieldNames = isset($this->documentFieldNames[$repo->getClassName()]) ?
167
            $this->documentFieldNames[$repo->getClassName()] :
168
            [];
169
170
        if ($online) {
171
            $languages = array_map(
172
                function (Language $language) {
173
                    return $language->getId();
174
                },
175
                $this->languageRepository->findAll()
176
            );
177
        } else {
178
            $languages = [
179
                $this->defaultLocale
180
            ];
181
        }
182
183
        // exposed events..
184
        $classShortName = $documentReflection->getShortName();
185
        if (isset($this->eventMap[$classShortName])) {
186
            $schema->setEventNames(array_unique($this->eventMap[$classShortName]['events']));
187
        }
188
189
        foreach ($meta->getFieldNames() as $field) {
190
            // don't describe hidden fields
191
            if (!isset($documentFieldNames[$field])) {
192
                continue;
193
            }
194
            // hide realId field (I was aiming at a cleaner solution than the macig realId string initially)
195
            if ($meta->getTypeOfField($field) == 'id' && $field == 'realId') {
196
                continue;
197
            }
198
199
            $property = new Schema();
200
            $property->setTitle($model->getTitleOfField($field));
201
            $property->setDescription($model->getDescriptionOfField($field));
202
203
            $property->setType($meta->getTypeOfField($field));
204
            $property->setReadOnly($model->getReadOnlyOfField($field));
205
206
            if ($meta->getTypeOfField($field) === 'many') {
207
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
208
209
                if ($model->hasDynamicKey($field)) {
210
                    $property->setType('object');
211
212
                    if ($online) {
213
                        // we generate a complete list of possible keys when we have access to mongodb
214
                        // this makes everything work with most json-schema v3 implementations (ie. schemaform.io)
215
                        $dynamicKeySpec = $model->getDynamicKeySpec($field);
216
217
                        $documentId = $dynamicKeySpec->{'document-id'};
218
                        $dynamicRepository = $this->repositoryFactory->get($documentId);
219
220
                        $repositoryMethod = $dynamicKeySpec->{'repository-method'};
221
                        $records = $dynamicRepository->$repositoryMethod();
222
223
                        $dynamicProperties = array_map(
224
                            function ($record) {
225
                                return $record->getId();
226
                            },
227
                            $records
228
                        );
229
                        foreach ($dynamicProperties as $propertyName) {
230
                            $property->addProperty(
231
                                $propertyName,
232
                                $this->getModelSchema($field, $propertyModel, $online)
233
                            );
234
                        }
235
                    } else {
236
                        // in the swagger case we can use additionPorerties which where introduced by json-schema v4
237
                        $property->setAdditionalProperties($this->getModelSchema($field, $propertyModel, $online));
238
                    }
239
                } else {
240
                    $property->setItems($this->getModelSchema($field, $propertyModel, $online));
241
                    $property->setType('array');
242
                }
243
            } elseif ($meta->getTypeOfField($field) === 'one') {
244
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
245
                $property = $this->getModelSchema($field, $propertyModel, $online);
246
            } elseif (in_array($field, $translatableFields, true)) {
247
                $property = $this->makeTranslatable($property, $languages);
248
            } elseif (in_array($field.'[]', $translatableFields, true)) {
249
                $property = $this->makeArrayTranslatable($property, $languages);
250
            } elseif ($meta->getTypeOfField($field) === 'extref') {
251
                $urls = array();
252
                $refCollections = $model->getRefCollectionOfField($field);
253
                foreach ($refCollections as $collection) {
254
                    if (isset($this->extrefServiceMapping[$collection])) {
255
                        $urls[] = $this->router->generate(
256
                            $this->extrefServiceMapping[$collection].'.all',
257
                            [],
258
                            true
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
259
                        );
260
                    } elseif ($collection === '*') {
261
                        $urls[] = '*';
262
                    }
263
                }
264
                $property->setRefCollection($urls);
265 View Code Duplication
            } elseif ($meta->getTypeOfField($field) === 'collection') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
266
                $itemSchema = new Schema();
267
                $property->setType('array');
268
                $itemSchema->setType($this->getCollectionItemType($meta->name, $field));
269
270
                $property->setItems($itemSchema);
271
                $property->setFormat(null);
272
            } elseif ($meta->getTypeOfField($field) === 'datearray') {
273
                $itemSchema = new Schema();
274
                $property->setType('array');
275
                $itemSchema->setType('string');
276
                $itemSchema->setFormat('date-time');
277
278
                $property->setItems($itemSchema);
279
                $property->setFormat(null);
280 View Code Duplication
            } elseif ($meta->getTypeOfField($field) === 'hasharray') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
281
                $itemSchema = new Schema();
282
                $itemSchema->setType('object');
283
284
                $property->setType('array');
285
                $property->setItems($itemSchema);
286
                $property->setFormat(null);
287
            }
288
            $schema->addProperty($documentFieldNames[$field], $property);
289
        }
290
291
        if ($meta->isEmbeddedDocument && !in_array('id', $model->getRequiredFields())) {
292
            $schema->removeProperty('id');
293
        }
294
295
        $requiredFields = [];
296
        foreach ($model->getRequiredFields() as $field) {
297
            // don't describe hidden fields
298
            if (!isset($documentFieldNames[$field])) {
299
                continue;
300
            }
301
302
            $requiredFields[] = $documentFieldNames[$field];
303
        }
304
        $schema->setRequired($requiredFields);
305
306
        return $schema;
307
    }
308
309
    /**
310
     * turn a property into a translatable property
311
     *
312
     * @param Schema   $property  simple string property
313
     * @param string[] $languages available languages
314
     *
315
     * @return Schema
316
     */
317
    public function makeTranslatable(Schema $property, $languages)
318
    {
319
        $property->setType('object');
320
        $property->setTranslatable(true);
321
322
        array_walk(
323
            $languages,
324
            function ($language) use ($property) {
325
                $schema = new Schema;
326
                $schema->setType('string');
327
                $schema->setTitle('Translated String');
328
                $schema->setDescription('String in ' . $language . ' locale.');
329
                $property->addProperty($language, $schema);
330
            }
331
        );
332
        $property->setRequired(['en']);
333
        return $property;
334
    }
335
336
    /**
337
     * turn a array property into a translatable property
338
     *
339
     * @param Schema   $property  simple string property
340
     * @param string[] $languages available languages
341
     *
342
     * @return Schema
343
     */
344
    public function makeArrayTranslatable(Schema $property, $languages)
345
    {
346
        $property->setType('array');
347
        $property->setItems($this->makeTranslatable(new Schema(), $languages));
348
        return $property;
349
    }
350
351
    /**
352
     * get canonical route to a schema based on a route
353
     *
354
     * @param string $routeName route name
355
     *
356
     * @return string schema route name
357
     */
358 6
    public static function getSchemaRouteName($routeName)
359
    {
360 6
        $routeParts = explode('.', $routeName);
361
362 6
        $routeType = array_pop($routeParts);
363
        // check if we need to create an item or collection schema
364 6
        $realRouteType = 'canonicalSchema';
365 6
        if ($routeType != 'options' && $routeType != 'all') {
366 5
            $realRouteType = 'canonicalIdSchema';
367 5
        }
368
369 6
        return implode('.', array_merge($routeParts, array($realRouteType)));
370
    }
371
372
    /**
373
     * Get item type of collection field
374
     *
375
     * @param string $className Class name
376
     * @param string $fieldName Field name
377
     * @return string|null
378
     */
379
    private function getCollectionItemType($className, $fieldName)
380
    {
381
        $serializerMetadata = $this->serializerMetadataFactory->getMetadataForClass($className);
382
        if ($serializerMetadata === null) {
383
            return null;
384
        }
385
        if (!isset($serializerMetadata->propertyMetadata[$fieldName])) {
386
            return null;
387
        }
388
389
        $type = $serializerMetadata->propertyMetadata[$fieldName]->type;
390
        return isset($type['name'], $type['params'][0]['name']) && $type['name'] === 'array' ?
391
            $type['params'][0]['name'] :
392
            null;
393
    }
394
}
395