Completed
Push — feature/other-validation ( 44262a...ed39e0 )
by Narcotic
10:20
created

SchemaUtils   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 477
Duplicated Lines 3.35 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 56.41%

Importance

Changes 15
Bugs 2 Features 4
Metric Value
wmc 55
c 15
b 2
f 4
lcom 1
cbo 13
dl 16
loc 477
rs 6.8
ccs 132
cts 234
cp 0.5641

7 Methods

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

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 Doctrine\Common\Cache\CacheProvider;
9
use Graviton\I18nBundle\Document\TranslatableDocumentInterface;
10
use Graviton\I18nBundle\Document\Language;
11
use Graviton\I18nBundle\Repository\LanguageRepository;
12
use Graviton\RestBundle\Model\DocumentModel;
13
use Graviton\SchemaBundle\Constraint\ConstraintBuilder;
14
use Graviton\SchemaBundle\Document\Schema;
15
use Graviton\SchemaBundle\Document\SchemaAdditionalProperties;
16
use Graviton\SchemaBundle\Document\SchemaType;
17
use Graviton\SchemaBundle\Service\RepositoryFactory;
18
use Metadata\MetadataFactoryInterface as SerializerMetadataFactoryInterface;
19
use Symfony\Component\Routing\RouterInterface;
20
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
21
22
/**
23
 * Utils for generating schemas.
24
 *
25
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
26
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
27
 * @link     http://swisscom.ch
28
 */
29
class SchemaUtils
30
{
31
32
    /**
33
     * language repository
34
     *
35
     * @var LanguageRepository repository
36
     */
37
    private $languageRepository;
38
39
    /**
40
     * router
41
     *
42
     * @var RouterInterface router
43
     */
44
    private $router;
45
46
    /**
47
     * mapping service names => route names
48
     *
49
     * @var array service mapping
50
     */
51
    private $extrefServiceMapping;
52
53
    /**
54
     * event map
55
     *
56
     * @var array event map
57
     */
58
    private $eventMap;
59
60
    /**
61
     * @var array [document class => [field name -> exposed name]]
62
     */
63
    private $documentFieldNames;
64
65
    /**
66
     * @var string
67
     */
68
    private $defaultLocale;
69
70
    /**
71
     * @var RepositoryFactory
72
     */
73
    private $repositoryFactory;
74
75
    /**
76
     * @var SerializerMetadataFactoryInterface
77
     */
78
    private $serializerMetadataFactory;
79
80
    /**
81
     * @var CacheProvider
82
     */
83
    private $cache;
84
85
    /**
86
     * @var string
87
     */
88
    private $cacheInvalidationMapKey;
89
90
    /**
91
     * @var ConstraintBuilder
92
     */
93
    private $constraintBuilder;
94
95
    /**
96
     * Constructor
97
     *
98
     * @param RepositoryFactory                  $repositoryFactory         Create repos from model class names
99
     * @param SerializerMetadataFactoryInterface $serializerMetadataFactory Serializer metadata factory
100
     * @param LanguageRepository                 $languageRepository        repository
101
     * @param RouterInterface                    $router                    router
102
     * @param array                              $extrefServiceMapping      Extref service mapping
103
     * @param array                              $eventMap                  eventmap
104
     * @param array                              $documentFieldNames        Document field names
105
     * @param string                             $defaultLocale             Default Language
106
     * @param ConstraintBuilder                  $constraintBuilder         Constraint builder
107
     * @param CacheProvider                      $cache                     Doctrine cache provider
108
     * @param string                             $cacheInvalidationMapKey   Cache invalidation map cache key
109
     */
110 6
    public function __construct(
111
        RepositoryFactory $repositoryFactory,
112
        SerializerMetadataFactoryInterface $serializerMetadataFactory,
113
        LanguageRepository $languageRepository,
114
        RouterInterface $router,
115
        array $extrefServiceMapping,
116
        array $eventMap,
117
        array $documentFieldNames,
118
        $defaultLocale,
119
        ConstraintBuilder $constraintBuilder,
120
        CacheProvider $cache,
121
        $cacheInvalidationMapKey
122
    ) {
123 6
        $this->repositoryFactory = $repositoryFactory;
124 6
        $this->serializerMetadataFactory = $serializerMetadataFactory;
125 6
        $this->languageRepository = $languageRepository;
126 6
        $this->router = $router;
127 6
        $this->extrefServiceMapping = $extrefServiceMapping;
128 6
        $this->eventMap = $eventMap;
129 6
        $this->documentFieldNames = $documentFieldNames;
130 6
        $this->defaultLocale = $defaultLocale;
131 6
        $this->constraintBuilder = $constraintBuilder;
132 6
        $this->cache = $cache;
133 6
        $this->cacheInvalidationMapKey = $cacheInvalidationMapKey;
134 6
    }
135
136
    /**
137
     * get schema for an array of models
138
     *
139
     * @param string        $modelName name of model
140
     * @param DocumentModel $model     model
141
     *
142
     * @return Schema
143
     */
144
    public function getCollectionSchema($modelName, DocumentModel $model)
145
    {
146
        $collectionSchema = new Schema;
147
        $collectionSchema->setTitle(sprintf('Array of %s objects', $modelName));
148
        $collectionSchema->setType('array');
149
150
        $collectionSchema->setItems($this->getModelSchema($modelName, $model));
151
152
        return $collectionSchema;
153
    }
154
155
    /**
156
     * return the schema for a given route
157
     *
158
     * @param string        $modelName name of mode to generate schema for
159
     * @param DocumentModel $model     model to generate schema for
160
     * @param boolean       $online    if we are online and have access to mongodb during this build
161
     * @param boolean       $internal  if true, we generate the schema for internal validation use
162
     *
163
     * @return Schema
164
     */
165 4
    public function getModelSchema($modelName, DocumentModel $model, $online = true, $internal = false)
166
    {
167 4
        $cacheKey = 'schema'.$model->getEntityClass().'.'.(string) $online.'.'.(string) $internal;
168
169 4
        if ($this->cache->contains($cacheKey)) {
170 2
            return $this->cache->fetch($cacheKey);
171
        }
172
173 2
        $invalidateCacheMap = [];
174 2
        if ($this->cache->contains($this->cacheInvalidationMapKey)) {
175
            $invalidateCacheMap = $this->cache->fetch($this->cacheInvalidationMapKey);
176
        }
177
178
        // build up schema data
179 2
        $schema = new Schema;
180
181 2
        if (!empty($model->getTitle())) {
182
            $schema->setTitle($model->getTitle());
183
        } else {
184 2
            if (!is_null($modelName)) {
185 2
                $schema->setTitle(ucfirst($modelName));
186 1
            } else {
187 2
                $reflection = new \ReflectionClass($model);
188 2
                $schema->setTitle(ucfirst($reflection->getShortName()));
189
            }
190 1
        }
191
192 2
        $schema->setDescription($model->getDescription());
193 2
        $schema->setDocumentClass($model->getDocumentClass());
0 ignored issues
show
Security Bug introduced by
It seems like $model->getDocumentClass() targeting Graviton\SchemaBundle\Mo...del::getDocumentClass() can also be of type false; however, Graviton\SchemaBundle\Do...ema::setDocumentClass() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
194 2
        $schema->setType('object');
195
196
        // grab schema info from model
197 2
        $repo = $model->getRepository();
198 2
        $meta = $repo->getClassMetadata();
199
200
        // Init sub searchable fields
201 2
        $subSearchableFields = array();
202
203
        // look for translatables in document class
204 2
        $documentReflection = new \ReflectionClass($repo->getClassName());
205 2
        if ($documentReflection->implementsInterface('Graviton\I18nBundle\Document\TranslatableDocumentInterface')) {
206
            /** @var TranslatableDocumentInterface $documentInstance */
207 2
            $documentInstance = $documentReflection->newInstanceWithoutConstructor();
208 2
            $translatableFields = array_merge(
209 2
                $documentInstance->getTranslatableFields(),
210 2
                $documentInstance->getPreTranslatedFields()
211 1
            );
212 1
        } else {
213
            $translatableFields = [];
214
        }
215
216 2
        if (!empty($translatableFields)) {
217
            $invalidateCacheMap[$this->languageRepository->getClassName()][] = $cacheKey;
218
        }
219
220
        // exposed fields
221 2
        $documentFieldNames = isset($this->documentFieldNames[$repo->getClassName()]) ?
222 2
            $this->documentFieldNames[$repo->getClassName()] :
223 2
            [];
224
225 2
        $languages = [];
226 2
        if ($online) {
227 2
            $languages = array_map(
228
                function (Language $language) {
229
                    return $language->getId();
230 2
                },
231 2
                $this->languageRepository->findAll()
232 1
            );
233 1
        }
234 2
        if (empty($languages)) {
235
            $languages = [
236 2
                $this->defaultLocale
237 1
            ];
238 1
        }
239
240
        // exposed events..
241 2
        $classShortName = $documentReflection->getShortName();
242 2
        if (isset($this->eventMap[$classShortName])) {
243 2
            $schema->setEventNames(array_unique($this->eventMap[$classShortName]['events']));
244 1
        }
245
246 2
        $requiredFields = [];
247 2
        $modelRequiredFields = $model->getRequiredFields();
248 2
        if (is_array($modelRequiredFields)) {
249 2
            foreach ($modelRequiredFields as $field) {
250
                // don't describe hidden fields
251
                if (!isset($documentFieldNames[$field])) {
252
                    continue;
253
                }
254
255
                $requiredFields[] = $documentFieldNames[$field];
256 1
            }
257 1
        }
258
259 2
        foreach ($meta->getFieldNames() as $field) {
260
            // don't describe hidden fields
261 2
            if (!isset($documentFieldNames[$field])) {
262
                continue;
263
            }
264
            // hide realId field (I was aiming at a cleaner solution than the macig realId string initially)
265 2
            if ($meta->getTypeOfField($field) == 'id' && $field == 'realId') {
266
                continue;
267
            }
268
269 2
            $property = new Schema();
270 2
            $property->setTitle($model->getTitleOfField($field));
271 2
            $property->setDescription($model->getDescriptionOfField($field));
272
273 2
            $property->setType($meta->getTypeOfField($field));
274 2
            $property->setReadOnly($model->getReadOnlyOfField($field));
275
276 2
            if ($meta->getTypeOfField($field) === 'many') {
277 1
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
278
279
                if ($model->hasDynamicKey($field)) {
280
                    $property->setType('object');
281
282
                    if ($online) {
283
                        // we generate a complete list of possible keys when we have access to mongodb
284
                        // this makes everything work with most json-schema v3 implementations (ie. schemaform.io)
285
                        $dynamicKeySpec = $model->getDynamicKeySpec($field);
286
287
                        $documentId = $dynamicKeySpec->{'document-id'};
288
                        $dynamicRepository = $this->repositoryFactory->get($documentId);
289
290
                        // put this in invalidate map so when know we have to invalidate when this document is used
291
                        $invalidateCacheMap[$dynamicRepository->getDocumentName()][] = $cacheKey;
0 ignored issues
show
Bug introduced by
The method getDocumentName() does not seem to exist on object<Doctrine\Common\Persistence\ObjectManager>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
292
293
                        $repositoryMethod = $dynamicKeySpec->{'repository-method'};
294
                        $records = $dynamicRepository->$repositoryMethod();
295
296
                        $dynamicProperties = array_map(
297
                            function ($record) {
298
                                return $record->getId();
299
                            },
300
                            $records
301
                        );
302
                        foreach ($dynamicProperties as $propertyName) {
303
                            $property->addProperty(
304
                                $propertyName,
305
                                $this->getModelSchema($field, $propertyModel, $online)
306
                            );
307
                        }
308
                    } else {
309
                        // swagger case
310
                        $property->setAdditionalProperties(
311
                            new SchemaAdditionalProperties($this->getModelSchema($field, $propertyModel, $online))
312
                        );
313
                    }
314
                } else {
315
                    $property->setItems($this->getModelSchema($field, $propertyModel, $online));
316
                    $property->setType('array');
317
                }
318 2
            } elseif ($meta->getTypeOfField($field) === 'one') {
319 2
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
320 2
                $property = $this->getModelSchema($field, $propertyModel, $online);
321
322 2
                if ($property->getSearchable()) {
323
                    foreach ($property->getSearchable() as $searchableSubField) {
324 1
                        $subSearchableFields[] = $field . '.' . $searchableSubField;
325
                    }
326
                }
327 2
            } elseif (in_array($field, $translatableFields, true)) {
328
                $property = $this->makeTranslatable($property, $languages);
329 2
            } elseif (in_array($field.'[]', $translatableFields, true)) {
330
                $property = $this->makeArrayTranslatable($property, $languages);
331 2
            } elseif ($meta->getTypeOfField($field) === 'extref') {
332
                $urls = array();
333
                $refCollections = $model->getRefCollectionOfField($field);
334
                foreach ($refCollections as $collection) {
335
                    if (isset($this->extrefServiceMapping[$collection])) {
336
                        $urls[] = $this->router->generate(
337
                            $this->extrefServiceMapping[$collection].'.all',
338
                            [],
339
                            UrlGeneratorInterface::ABSOLUTE_URL
340
                        );
341
                    } elseif ($collection === '*') {
342
                        $urls[] = '*';
343
                    }
344
                }
345
                $property->setRefCollection($urls);
346 2 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...
347 2
                $itemSchema = new Schema();
348 2
                $property->setType('array');
349 2
                $itemSchema->setType($this->getCollectionItemType($meta->name, $field));
350
351 2
                $property->setItems($itemSchema);
352 2
                $property->setFormat(null);
353 2
            } elseif ($meta->getTypeOfField($field) === 'datearray') {
354
                $itemSchema = new Schema();
355
                $property->setType('array');
356
                $itemSchema->setType('string');
357
                $itemSchema->setFormat('date-time');
358
359
                $property->setItems($itemSchema);
360
                $property->setFormat(null);
361 2 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...
362
                $itemSchema = new Schema();
363
                $itemSchema->setType('object');
364
365
                $property->setType('array');
366
                $property->setItems($itemSchema);
367
                $property->setFormat(null);
368
            }
369
370 2
            if (in_array($meta->getTypeOfField($field), $property->getMinLengthTypes())) {
371
                // make sure a required field cannot be blank
372 2
                if (in_array($documentFieldNames[$field], $requiredFields)) {
373
                    $property->setMinLength(1);
374
                } else {
375
                    // in the other case, make sure also null can be sent..
376 2
                    $currentType = $property->getType();
377 2
                    if ($currentType instanceof SchemaType) {
378 2
                        $property->setType(array_merge($currentType->getTypes(), ['null']));
379 1
                    } else {
380
                        $property->setType('null');
381
                    }
382
                }
383 1
            }
384
385 2
            $property = $this->constraintBuilder->addConstraints($field, $property, $model);
386
387 2
            $schema->addProperty($documentFieldNames[$field], $property);
388 1
        }
389
390 2
        if ($meta->isEmbeddedDocument && !in_array('id', $model->getRequiredFields())) {
391 2
            $schema->removeProperty('id');
392 1
        }
393
394
        /**
395
         * if we generate schema for internal use; don't have id in required array as
396
         * it's 'requiredness' depends on the method used (POST/PUT/PATCH) and is checked in checks
397
         * before validation.
398
         */
399 2
        $idPosition = array_search('id', $requiredFields);
400 2
        if ($internal === true && $idPosition !== false) {
401
            unset($requiredFields[$idPosition]);
402
        }
403
404 2
        $schema->setRequired($requiredFields);
405
406
        // set additionalProperties to false (as this is our default policy) if not already set
407 2
        if (is_null($schema->getAdditionalProperties()) && $online) {
408 2
            $schema->setAdditionalProperties(new SchemaAdditionalProperties(false));
409 1
        }
410
411 2
        $searchableFields = array_merge($subSearchableFields, $model->getSearchableFields());
412 2
        $schema->setSearchable($searchableFields);
413
414 2
        $this->cache->save($cacheKey, $schema);
415 2
        $this->cache->save($this->cacheInvalidationMapKey, $invalidateCacheMap);
416
417 2
        return $schema;
418
    }
419
420
    /**
421
     * turn a property into a translatable property
422
     *
423
     * @param Schema   $property  simple string property
424
     * @param string[] $languages available languages
425
     *
426
     * @return Schema
427
     */
428
    public function makeTranslatable(Schema $property, $languages)
429
    {
430
        $property->setType('object');
431
        $property->setTranslatable(true);
432
433
        array_walk(
434
            $languages,
435
            function ($language) use ($property) {
436
                $schema = new Schema;
437
                $schema->setType('string');
438
                $schema->setTitle('Translated String');
439
                $schema->setDescription('String in ' . $language . ' locale.');
440
                $property->addProperty($language, $schema);
441
            }
442
        );
443
        $property->setRequired(['en']);
444
        return $property;
445
    }
446
447
    /**
448
     * turn a array property into a translatable property
449
     *
450
     * @param Schema   $property  simple string property
451
     * @param string[] $languages available languages
452
     *
453
     * @return Schema
454
     */
455
    public function makeArrayTranslatable(Schema $property, $languages)
456
    {
457
        $property->setType('array');
458
        $property->setItems($this->makeTranslatable(new Schema(), $languages));
459
        return $property;
460
    }
461
462
    /**
463
     * get canonical route to a schema based on a route
464
     *
465
     * @param string $routeName route name
466
     *
467
     * @return string schema route name
468
     */
469 4
    public static function getSchemaRouteName($routeName)
470
    {
471 4
        $routeParts = explode('.', $routeName);
472
473 4
        $routeType = array_pop($routeParts);
474
        // check if we need to create an item or collection schema
475 4
        $realRouteType = 'canonicalSchema';
476 4
        if ($routeType != 'options' && $routeType != 'all') {
477 4
            $realRouteType = 'canonicalIdSchema';
478 2
        }
479
480 4
        return implode('.', array_merge($routeParts, array($realRouteType)));
481
    }
482
483
    /**
484
     * Get item type of collection field
485
     *
486
     * @param string $className Class name
487
     * @param string $fieldName Field name
488
     * @return string|null
489
     */
490 2
    private function getCollectionItemType($className, $fieldName)
491
    {
492 2
        $serializerMetadata = $this->serializerMetadataFactory->getMetadataForClass($className);
493 2
        if ($serializerMetadata === null) {
494
            return null;
495
        }
496 2
        if (!isset($serializerMetadata->propertyMetadata[$fieldName])) {
497
            return null;
498
        }
499
500 2
        $type = $serializerMetadata->propertyMetadata[$fieldName]->type;
501 2
        return isset($type['name'], $type['params'][0]['name']) && $type['name'] === 'array' ?
502 2
            $type['params'][0]['name'] :
503 2
            null;
504
    }
505
}
506