Completed
Push — master ( 11b317...37df4d )
by Lucas
09:27
created

SchemaUtils::getModelSchema()   F

Complexity

Conditions 31
Paths 1920

Size

Total Lines 190
Code Lines 119

Duplication

Lines 16
Ratio 8.42 %

Code Coverage

Tests 0
CRAP Score 992

Importance

Changes 4
Bugs 0 Features 0
Metric Value
dl 16
loc 190
rs 2
c 4
b 0
f 0
ccs 0
cts 136
cp 0
cc 31
eloc 119
nc 1920
nop 3
crap 992

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
 * 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
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
17
18
/**
19
 * Utils for generating schemas.
20
 *
21
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
22
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
23
 * @link     http://swisscom.ch
24
 */
25
class SchemaUtils
26
{
27
28
    /**
29
     * language repository
30
     *
31
     * @var LanguageRepository repository
32
     */
33
    private $languageRepository;
34
35
    /**
36
     * router
37
     *
38
     * @var RouterInterface router
39
     */
40
    private $router;
41
42
    /**
43
     * mapping service names => route names
44
     *
45
     * @var array service mapping
46
     */
47
    private $extrefServiceMapping;
48
49
    /**
50
     * event map
51
     *
52
     * @var array event map
53
     */
54
    private $eventMap;
55
56
    /**
57
     * @var array [document class => [field name -> exposed name]]
58
     */
59
    private $documentFieldNames;
60
61
    /**
62
     * @var string
63
     */
64
    private $defaultLocale;
65
66
    /**
67
     * @var RepositoryFactory
68
     */
69
    private $repositoryFactory;
70
71
    /**
72
     * @var SerializerMetadataFactoryInterface
73
     */
74
    private $serializerMetadataFactory;
75
76
    /**
77
     * Constructor
78
     *
79
     * @param RepositoryFactory                  $repositoryFactory         Create repos from model class names
80
     * @param SerializerMetadataFactoryInterface $serializerMetadataFactory Serializer metadata factory
81
     * @param LanguageRepository                 $languageRepository        repository
82
     * @param RouterInterface                    $router                    router
83
     * @param array                              $extrefServiceMapping      Extref service mapping
84
     * @param array                              $eventMap                  eventmap
85
     * @param array                              $documentFieldNames        Document field names
86
     * @param string                             $defaultLocale             Default Language
87
     */
88 2
    public function __construct(
89
        RepositoryFactory $repositoryFactory,
90
        SerializerMetadataFactoryInterface $serializerMetadataFactory,
91
        LanguageRepository $languageRepository,
92
        RouterInterface $router,
93
        array $extrefServiceMapping,
94
        array $eventMap,
95
        array $documentFieldNames,
96
        $defaultLocale
97
    ) {
98 2
        $this->repositoryFactory = $repositoryFactory;
99 2
        $this->serializerMetadataFactory = $serializerMetadataFactory;
100 2
        $this->languageRepository = $languageRepository;
101 2
        $this->router = $router;
102 2
        $this->extrefServiceMapping = $extrefServiceMapping;
103 2
        $this->eventMap = $eventMap;
104 2
        $this->documentFieldNames = $documentFieldNames;
105 2
        $this->defaultLocale = $defaultLocale;
106 2
    }
107
108
    /**
109
     * get schema for an array of models
110
     *
111
     * @param string        $modelName name of model
112
     * @param DocumentModel $model     model
113
     *
114
     * @return Schema
115
     */
116
    public function getCollectionSchema($modelName, DocumentModel $model)
117
    {
118
        $collectionSchema = new Schema;
119
        $collectionSchema->setTitle(sprintf('Array of %s objects', $modelName));
120
        $collectionSchema->setType('array');
121
122
        $collectionSchema->setItems($this->getModelSchema($modelName, $model));
123
124
        return $collectionSchema;
125
    }
126
127
    /**
128
     * return the schema for a given route
129
     *
130
     * @param string        $modelName name of mode to generate schema for
131
     * @param DocumentModel $model     model to generate schema for
132
     * @param boolean       $online    if we are online and have access to mongodb during this build
133
     *
134
     * @return Schema
135
     */
136
    public function getModelSchema($modelName, DocumentModel $model, $online = true)
137
    {
138
        // build up schema data
139
        $schema = new Schema;
140
141
        if (!empty($model->getTitle())) {
142
            $schema->setTitle($model->getTitle());
143
        } else {
144
            $schema->setTitle(ucfirst($modelName));
145
        }
146
147
        $schema->setDescription($model->getDescription());
148
        $schema->setType('object');
149
150
        // grab schema info from model
151
        $repo = $model->getRepository();
152
        $meta = $repo->getClassMetadata();
153
154
        // Init sub searchable fields
155
        $subSearchableFields = array();
156
157
        // look for translatables in document class
158
        $documentReflection = new \ReflectionClass($repo->getClassName());
159
        if ($documentReflection->implementsInterface('Graviton\I18nBundle\Document\TranslatableDocumentInterface')) {
160
            /** @var TranslatableDocumentInterface $documentInstance */
161
            $documentInstance = $documentReflection->newInstanceWithoutConstructor();
162
            $translatableFields = array_merge(
163
                $documentInstance->getTranslatableFields(),
164
                $documentInstance->getPreTranslatedFields()
165
            );
166
        } else {
167
            $translatableFields = [];
168
        }
169
170
        // exposed fields
171
        $documentFieldNames = isset($this->documentFieldNames[$repo->getClassName()]) ?
172
            $this->documentFieldNames[$repo->getClassName()] :
173
            [];
174
175
        if ($online) {
176
            $languages = array_map(
177
                function (Language $language) {
178
                    return $language->getId();
179
                },
180
                $this->languageRepository->findAll()
181
            );
182
        } else {
183
            $languages = [
184
                $this->defaultLocale
185
            ];
186
        }
187
188
        // exposed events..
189
        $classShortName = $documentReflection->getShortName();
190
        if (isset($this->eventMap[$classShortName])) {
191
            $schema->setEventNames(array_unique($this->eventMap[$classShortName]['events']));
192
        }
193
194
        foreach ($meta->getFieldNames() as $field) {
195
            // don't describe hidden fields
196
            if (!isset($documentFieldNames[$field])) {
197
                continue;
198
            }
199
            // hide realId field (I was aiming at a cleaner solution than the macig realId string initially)
200
            if ($meta->getTypeOfField($field) == 'id' && $field == 'realId') {
201
                continue;
202
            }
203
204
            $property = new Schema();
205
            $property->setTitle($model->getTitleOfField($field));
206
            $property->setDescription($model->getDescriptionOfField($field));
207
208
            $property->setType($meta->getTypeOfField($field));
209
            $property->setReadOnly($model->getReadOnlyOfField($field));
210
211
            if ($meta->getTypeOfField($field) === 'many') {
212
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
213
214
                if ($model->hasDynamicKey($field)) {
215
                    $property->setType('object');
216
217
                    if ($online) {
218
                        // we generate a complete list of possible keys when we have access to mongodb
219
                        // this makes everything work with most json-schema v3 implementations (ie. schemaform.io)
220
                        $dynamicKeySpec = $model->getDynamicKeySpec($field);
221
222
                        $documentId = $dynamicKeySpec->{'document-id'};
223
                        $dynamicRepository = $this->repositoryFactory->get($documentId);
224
225
                        $repositoryMethod = $dynamicKeySpec->{'repository-method'};
226
                        $records = $dynamicRepository->$repositoryMethod();
227
228
                        $dynamicProperties = array_map(
229
                            function ($record) {
230
                                return $record->getId();
231
                            },
232
                            $records
233
                        );
234
                        foreach ($dynamicProperties as $propertyName) {
235
                            $property->addProperty(
236
                                $propertyName,
237
                                $this->getModelSchema($field, $propertyModel, $online)
238
                            );
239
                        }
240
                    } else {
241
                        // in the swagger case we can use additionPorerties which where introduced by json-schema v4
242
                        $property->setAdditionalProperties($this->getModelSchema($field, $propertyModel, $online));
243
                    }
244
                } else {
245
                    $property->setItems($this->getModelSchema($field, $propertyModel, $online));
246
                    $property->setType('array');
247
                }
248
            } elseif ($meta->getTypeOfField($field) === 'one') {
249
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
250
                $property = $this->getModelSchema($field, $propertyModel, $online);
251
252
                if ($property->getSearchable()) {
253
                    foreach ($property->getSearchable() as $searchableSubField) {
254
                        $subSearchableFields[] = $field . '.' . $searchableSubField;
255
                    }
256
                }
257
            } elseif (in_array($field, $translatableFields, true)) {
258
                $property = $this->makeTranslatable($property, $languages);
259
            } elseif (in_array($field.'[]', $translatableFields, true)) {
260
                $property = $this->makeArrayTranslatable($property, $languages);
261
            } elseif ($meta->getTypeOfField($field) === 'extref') {
262
                $urls = array();
263
                $refCollections = $model->getRefCollectionOfField($field);
264
                foreach ($refCollections as $collection) {
265
                    if (isset($this->extrefServiceMapping[$collection])) {
266
                        $urls[] = $this->router->generate(
267
                            $this->extrefServiceMapping[$collection].'.all',
268
                            [],
269
                            UrlGeneratorInterface::ABSOLUTE_URL
270
                        );
271
                    } elseif ($collection === '*') {
272
                        $urls[] = '*';
273
                    }
274
                }
275
                $property->setRefCollection($urls);
276 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...
277
                $itemSchema = new Schema();
278
                $property->setType('array');
279
                $itemSchema->setType($this->getCollectionItemType($meta->name, $field));
280
281
                $property->setItems($itemSchema);
282
                $property->setFormat(null);
283
            } elseif ($meta->getTypeOfField($field) === 'datearray') {
284
                $itemSchema = new Schema();
285
                $property->setType('array');
286
                $itemSchema->setType('string');
287
                $itemSchema->setFormat('date-time');
288
289
                $property->setItems($itemSchema);
290
                $property->setFormat(null);
291 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...
292
                $itemSchema = new Schema();
293
                $itemSchema->setType('object');
294
295
                $property->setType('array');
296
                $property->setItems($itemSchema);
297
                $property->setFormat(null);
298
            }
299
            $schema->addProperty($documentFieldNames[$field], $property);
300
        }
301
302
        if ($meta->isEmbeddedDocument && !in_array('id', $model->getRequiredFields())) {
303
            $schema->removeProperty('id');
304
        }
305
306
        $requiredFields = [];
307
        $modelRequiredFields = $model->getRequiredFields();
308
        if (is_array($modelRequiredFields)) {
309
            foreach ($modelRequiredFields as $field) {
310
                // don't describe hidden fields
311
                if (!isset($documentFieldNames[$field])) {
312
                    continue;
313
                }
314
315
                $requiredFields[] = $documentFieldNames[$field];
316
            }
317
        }
318
        $schema->setRequired($requiredFields);
319
320
        $searchableFields = array_merge($subSearchableFields, $model->getSearchableFields());
321
322
        $schema->setSearchable($searchableFields);
323
324
        return $schema;
325
    }
326
327
    /**
328
     * turn a property into a translatable property
329
     *
330
     * @param Schema   $property  simple string property
331
     * @param string[] $languages available languages
332
     *
333
     * @return Schema
334
     */
335
    public function makeTranslatable(Schema $property, $languages)
336
    {
337
        $property->setType('object');
338
        $property->setTranslatable(true);
339
340
        array_walk(
341
            $languages,
342
            function ($language) use ($property) {
343
                $schema = new Schema;
344
                $schema->setType('string');
345
                $schema->setTitle('Translated String');
346
                $schema->setDescription('String in ' . $language . ' locale.');
347
                $property->addProperty($language, $schema);
348
            }
349
        );
350
        $property->setRequired(['en']);
351
        return $property;
352
    }
353
354
    /**
355
     * turn a array property into a translatable property
356
     *
357
     * @param Schema   $property  simple string property
358
     * @param string[] $languages available languages
359
     *
360
     * @return Schema
361
     */
362
    public function makeArrayTranslatable(Schema $property, $languages)
363
    {
364
        $property->setType('array');
365
        $property->setItems($this->makeTranslatable(new Schema(), $languages));
366
        return $property;
367
    }
368
369
    /**
370
     * get canonical route to a schema based on a route
371
     *
372
     * @param string $routeName route name
373
     *
374
     * @return string schema route name
375
     */
376
    public static function getSchemaRouteName($routeName)
377
    {
378
        $routeParts = explode('.', $routeName);
379
380
        $routeType = array_pop($routeParts);
381
        // check if we need to create an item or collection schema
382
        $realRouteType = 'canonicalSchema';
383
        if ($routeType != 'options' && $routeType != 'all') {
384
            $realRouteType = 'canonicalIdSchema';
385
        }
386
387
        return implode('.', array_merge($routeParts, array($realRouteType)));
388
    }
389
390
    /**
391
     * Get item type of collection field
392
     *
393
     * @param string $className Class name
394
     * @param string $fieldName Field name
395
     * @return string|null
396
     */
397
    private function getCollectionItemType($className, $fieldName)
398
    {
399
        $serializerMetadata = $this->serializerMetadataFactory->getMetadataForClass($className);
400
        if ($serializerMetadata === null) {
401
            return null;
402
        }
403
        if (!isset($serializerMetadata->propertyMetadata[$fieldName])) {
404
            return null;
405
        }
406
407
        $type = $serializerMetadata->propertyMetadata[$fieldName]->type;
408
        return isset($type['name'], $type['params'][0]['name']) && $type['name'] === 'array' ?
409
            $type['params'][0]['name'] :
410
            null;
411
    }
412
}
413