Completed
Push — feature/other-validation ( 15ee30 )
by Narcotic
09:20
created

SchemaUtils   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 390
Duplicated Lines 4.1 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 5.26%

Importance

Changes 6
Bugs 0 Features 1
Metric Value
wmc 43
c 6
b 0
f 1
lcom 1
cbo 10
dl 16
loc 390
ccs 10
cts 190
cp 0.0526
rs 8.3157

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getCollectionSchema() 0 10 1
A __construct() 0 19 1
F getModelSchema() 16 192 31
A makeTranslatable() 0 18 1
A makeArrayTranslatable() 0 6 1
A getSchemaRouteName() 0 13 3
B getCollectionItemType() 0 15 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SchemaUtils often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchemaUtils, and based on these observations, apply Extract Interface, too.

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->setAdditionalProperties(new Schema);
148
149
        $schema->setDescription($model->getDescription());
150
        $schema->setType('object');
151
152
        // grab schema info from model
153
        $repo = $model->getRepository();
154
        $meta = $repo->getClassMetadata();
155
156
        // Init sub searchable fields
157
        $subSearchableFields = array();
158
159
        // look for translatables in document class
160
        $documentReflection = new \ReflectionClass($repo->getClassName());
161
        if ($documentReflection->implementsInterface('Graviton\I18nBundle\Document\TranslatableDocumentInterface')) {
162
            /** @var TranslatableDocumentInterface $documentInstance */
163
            $documentInstance = $documentReflection->newInstanceWithoutConstructor();
164
            $translatableFields = array_merge(
165
                $documentInstance->getTranslatableFields(),
166
                $documentInstance->getPreTranslatedFields()
167
            );
168
        } else {
169
            $translatableFields = [];
170
        }
171
172
        // exposed fields
173
        $documentFieldNames = isset($this->documentFieldNames[$repo->getClassName()]) ?
174
            $this->documentFieldNames[$repo->getClassName()] :
175
            [];
176
177
        if ($online) {
178
            $languages = array_map(
179
                function (Language $language) {
180
                    return $language->getId();
181
                },
182
                $this->languageRepository->findAll()
183
            );
184
        } else {
185
            $languages = [
186
                $this->defaultLocale
187
            ];
188
        }
189
190
        // exposed events..
191
        $classShortName = $documentReflection->getShortName();
192
        if (isset($this->eventMap[$classShortName])) {
193
            $schema->setEventNames(array_unique($this->eventMap[$classShortName]['events']));
194
        }
195
196
        foreach ($meta->getFieldNames() as $field) {
197
            // don't describe hidden fields
198
            if (!isset($documentFieldNames[$field])) {
199
                continue;
200
            }
201
            // hide realId field (I was aiming at a cleaner solution than the macig realId string initially)
202
            if ($meta->getTypeOfField($field) == 'id' && $field == 'realId') {
203
                continue;
204
            }
205
206
            $property = new Schema();
207
            $property->setTitle($model->getTitleOfField($field));
208
            $property->setDescription($model->getDescriptionOfField($field));
209
210
            $property->setType($meta->getTypeOfField($field));
211
            $property->setReadOnly($model->getReadOnlyOfField($field));
212
213
            if ($meta->getTypeOfField($field) === 'many') {
214
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
215
216
                if ($model->hasDynamicKey($field)) {
217
                    $property->setType('object');
218
219
                    if ($online) {
220
                        // we generate a complete list of possible keys when we have access to mongodb
221
                        // this makes everything work with most json-schema v3 implementations (ie. schemaform.io)
222
                        $dynamicKeySpec = $model->getDynamicKeySpec($field);
223
224
                        $documentId = $dynamicKeySpec->{'document-id'};
225
                        $dynamicRepository = $this->repositoryFactory->get($documentId);
226
227
                        $repositoryMethod = $dynamicKeySpec->{'repository-method'};
228
                        $records = $dynamicRepository->$repositoryMethod();
229
230
                        $dynamicProperties = array_map(
231
                            function ($record) {
232
                                return $record->getId();
233
                            },
234
                            $records
235
                        );
236
                        foreach ($dynamicProperties as $propertyName) {
237
                            $property->addProperty(
238
                                $propertyName,
239
                                $this->getModelSchema($field, $propertyModel, $online)
240
                            );
241
                        }
242
                    } else {
243
                        // in the swagger case we can use additionPorerties which where introduced by json-schema v4
244
                        $property->setAdditionalProperties($this->getModelSchema($field, $propertyModel, $online));
245
                    }
246
                } else {
247
                    $property->setItems($this->getModelSchema($field, $propertyModel, $online));
248
                    $property->setType('array');
249
                }
250
            } elseif ($meta->getTypeOfField($field) === 'one') {
251
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
252
                $property = $this->getModelSchema($field, $propertyModel, $online);
253
254
                if ($property->getSearchable()) {
255
                    foreach ($property->getSearchable() as $searchableSubField) {
256
                        $subSearchableFields[] = $field . '.' . $searchableSubField;
257
                    }
258
                }
259
            } elseif (in_array($field, $translatableFields, true)) {
260
                $property = $this->makeTranslatable($property, $languages);
261
            } elseif (in_array($field.'[]', $translatableFields, true)) {
262
                $property = $this->makeArrayTranslatable($property, $languages);
263
            } elseif ($meta->getTypeOfField($field) === 'extref') {
264
                $urls = array();
265
                $refCollections = $model->getRefCollectionOfField($field);
266
                foreach ($refCollections as $collection) {
267
                    if (isset($this->extrefServiceMapping[$collection])) {
268
                        $urls[] = $this->router->generate(
269
                            $this->extrefServiceMapping[$collection].'.all',
270
                            [],
271
                            UrlGeneratorInterface::ABSOLUTE_URL
272
                        );
273
                    } elseif ($collection === '*') {
274
                        $urls[] = '*';
275
                    }
276
                }
277
                $property->setRefCollection($urls);
278 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...
279
                $itemSchema = new Schema();
280
                $property->setType('array');
281
                $itemSchema->setType($this->getCollectionItemType($meta->name, $field));
282
283
                $property->setItems($itemSchema);
284
                $property->setFormat(null);
285
            } elseif ($meta->getTypeOfField($field) === 'datearray') {
286
                $itemSchema = new Schema();
287
                $property->setType('array');
288
                $itemSchema->setType('string');
289
                $itemSchema->setFormat('date-time');
290
291
                $property->setItems($itemSchema);
292
                $property->setFormat(null);
293 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...
294
                $itemSchema = new Schema();
295
                $itemSchema->setType('object');
296
297
                $property->setType('array');
298
                $property->setItems($itemSchema);
299
                $property->setFormat(null);
300
            }
301
            $schema->addProperty($documentFieldNames[$field], $property);
302
        }
303
304
        if ($meta->isEmbeddedDocument && !in_array('id', $model->getRequiredFields())) {
305
            $schema->removeProperty('id');
306
        }
307
308
        $requiredFields = [];
309
        $modelRequiredFields = $model->getRequiredFields();
310
        if (is_array($modelRequiredFields)) {
311
            foreach ($modelRequiredFields as $field) {
312
                // don't describe hidden fields
313
                if (!isset($documentFieldNames[$field])) {
314
                    continue;
315
                }
316
317
                $requiredFields[] = $documentFieldNames[$field];
318
            }
319
        }
320
        $schema->setRequired($requiredFields);
321
322
        $searchableFields = array_merge($subSearchableFields, $model->getSearchableFields());
323
324
        $schema->setSearchable($searchableFields);
325
326
        return $schema;
327
    }
328
329
    /**
330
     * turn a property into a translatable property
331
     *
332
     * @param Schema   $property  simple string property
333
     * @param string[] $languages available languages
334
     *
335
     * @return Schema
336
     */
337
    public function makeTranslatable(Schema $property, $languages)
338
    {
339
        $property->setType('object');
340
        $property->setTranslatable(true);
341
342
        array_walk(
343
            $languages,
344
            function ($language) use ($property) {
345
                $schema = new Schema;
346
                $schema->setType('string');
347
                $schema->setTitle('Translated String');
348
                $schema->setDescription('String in ' . $language . ' locale.');
349
                $property->addProperty($language, $schema);
350
            }
351
        );
352
        $property->setRequired(['en']);
353
        return $property;
354
    }
355
356
    /**
357
     * turn a array property into a translatable property
358
     *
359
     * @param Schema   $property  simple string property
360
     * @param string[] $languages available languages
361
     *
362
     * @return Schema
363
     */
364
    public function makeArrayTranslatable(Schema $property, $languages)
365
    {
366
        $property->setType('array');
367
        $property->setItems($this->makeTranslatable(new Schema(), $languages));
368
        return $property;
369
    }
370
371
    /**
372
     * get canonical route to a schema based on a route
373
     *
374
     * @param string $routeName route name
375
     *
376
     * @return string schema route name
377
     */
378
    public static function getSchemaRouteName($routeName)
379
    {
380
        $routeParts = explode('.', $routeName);
381
382
        $routeType = array_pop($routeParts);
383
        // check if we need to create an item or collection schema
384
        $realRouteType = 'canonicalSchema';
385
        if ($routeType != 'options' && $routeType != 'all') {
386
            $realRouteType = 'canonicalIdSchema';
387
        }
388
389
        return implode('.', array_merge($routeParts, array($realRouteType)));
390
    }
391
392
    /**
393
     * Get item type of collection field
394
     *
395
     * @param string $className Class name
396
     * @param string $fieldName Field name
397
     * @return string|null
398
     */
399
    private function getCollectionItemType($className, $fieldName)
400
    {
401
        $serializerMetadata = $this->serializerMetadataFactory->getMetadataForClass($className);
402
        if ($serializerMetadata === null) {
403
            return null;
404
        }
405
        if (!isset($serializerMetadata->propertyMetadata[$fieldName])) {
406
            return null;
407
        }
408
409
        $type = $serializerMetadata->propertyMetadata[$fieldName]->type;
410
        return isset($type['name'], $type['params'][0]['name']) && $type['name'] === 'array' ?
411
            $type['params'][0]['name'] :
412
            null;
413
    }
414
}
415