Completed
Push — feature/other-validation ( f56062...7f372e )
by Narcotic
65:46
created

SchemaUtils::__construct()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 28
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 1.0098

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 0
loc 28
rs 8.8571
ccs 11
cts 14
cp 0.7856
cc 1
eloc 25
nc 1
nop 12
crap 1.0098

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 2
     * @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                             $cacheNamespace            Cache namespace
109
     * @param string                             $cacheInvalidationMapKey   Cache invalidation map cache key
110
     */
111
    public function __construct(
112
        RepositoryFactory $repositoryFactory,
113
        SerializerMetadataFactoryInterface $serializerMetadataFactory,
114
        LanguageRepository $languageRepository,
115 2
        RouterInterface $router,
116 2
        array $extrefServiceMapping,
117 2
        array $eventMap,
118 2
        array $documentFieldNames,
119 2
        $defaultLocale,
120 2
        ConstraintBuilder $constraintBuilder,
121 2
        CacheProvider $cache,
122 2
        $cacheNamespace,
123 2
        $cacheInvalidationMapKey
124 2
    ) {
125 2
        $this->repositoryFactory = $repositoryFactory;
126
        $this->serializerMetadataFactory = $serializerMetadataFactory;
127
        $this->languageRepository = $languageRepository;
128
        $this->router = $router;
129
        $this->extrefServiceMapping = $extrefServiceMapping;
130
        $this->eventMap = $eventMap;
131
        $this->documentFieldNames = $documentFieldNames;
132
        $this->defaultLocale = $defaultLocale;
133
        $this->constraintBuilder = $constraintBuilder;
134
135
        $cache->setNamespace($cacheNamespace);
136
        $this->cache = $cache;
137
        $this->cacheInvalidationMapKey = $cacheInvalidationMapKey;
138
    }
139
140
    /**
141
     * get schema for an array of models
142
     *
143
     * @param string        $modelName name of model
144
     * @param DocumentModel $model     model
145
     *
146
     * @return Schema
147
     */
148
    public function getCollectionSchema($modelName, DocumentModel $model)
149
    {
150
        $collectionSchema = new Schema;
151
        $collectionSchema->setTitle(sprintf('Array of %s objects', $modelName));
152
        $collectionSchema->setType('array');
153
154
        $collectionSchema->setItems($this->getModelSchema($modelName, $model));
155
156
        return $collectionSchema;
157
    }
158
159
    /**
160
     * return the schema for a given route
161
     *
162
     * @param string        $modelName name of mode to generate schema for
163
     * @param DocumentModel $model     model to generate schema for
164
     * @param boolean       $online    if we are online and have access to mongodb during this build
165
     * @param boolean       $internal  if true, we generate the schema for internal validation use
166
     *
167
     * @return Schema
168
     */
169
    public function getModelSchema($modelName, DocumentModel $model, $online = true, $internal = false)
170
    {
171
        $cacheKey = $model->getEntityClass().'.'.(string) $online.'.'.(string) $internal.uniqid();
172
173
        if ($this->cache->contains($cacheKey)) {
174
            return $this->cache->fetch($cacheKey);
175
        }
176
177
        $invalidateCacheMap = [];
178
        if ($this->cache->contains($this->cacheInvalidationMapKey)) {
179
            $invalidateCacheMap = $this->cache->fetch($this->cacheInvalidationMapKey);
180
        }
181
182
        // build up schema data
183
        $schema = new Schema;
184
185
        if (!empty($model->getTitle())) {
186
            $schema->setTitle($model->getTitle());
187
        } else {
188
            if (!is_null($modelName)) {
189
                $schema->setTitle(ucfirst($modelName));
190
            } else {
191
                $reflection = new \ReflectionClass($model);
192
                $schema->setTitle(ucfirst($reflection->getShortName()));
193
            }
194
        }
195
196
        $schema->setDescription($model->getDescription());
197
        $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...
198
        $schema->setType('object');
199
200
        // grab schema info from model
201
        $repo = $model->getRepository();
202
        $meta = $repo->getClassMetadata();
203
204
        // Init sub searchable fields
205
        $subSearchableFields = array();
206
207
        // look for translatables in document class
208
        $documentReflection = new \ReflectionClass($repo->getClassName());
209
        if ($documentReflection->implementsInterface('Graviton\I18nBundle\Document\TranslatableDocumentInterface')) {
210
            /** @var TranslatableDocumentInterface $documentInstance */
211
            $documentInstance = $documentReflection->newInstanceWithoutConstructor();
212
            $translatableFields = array_merge(
213
                $documentInstance->getTranslatableFields(),
214
                $documentInstance->getPreTranslatedFields()
215
            );
216
        } else {
217
            $translatableFields = [];
218
        }
219
220
        if (!empty($translatableFields)) {
221
            $invalidateCacheMap[$this->languageRepository->getClassName()][] = $cacheKey;
222
        }
223
224
        // exposed fields
225
        $documentFieldNames = isset($this->documentFieldNames[$repo->getClassName()]) ?
226
            $this->documentFieldNames[$repo->getClassName()] :
227
            [];
228
229
        if ($online) {
230
            $languages = array_map(
231
                function (Language $language) {
232
                    return $language->getId();
233
                },
234
                $this->languageRepository->findAll()
235
            );
236
        } else {
237
            $languages = [
238
                $this->defaultLocale
239
            ];
240
        }
241
242
        // exposed events..
243
        $classShortName = $documentReflection->getShortName();
244
        if (isset($this->eventMap[$classShortName])) {
245
            $schema->setEventNames(array_unique($this->eventMap[$classShortName]['events']));
246
        }
247
248
        $requiredFields = [];
249
        $modelRequiredFields = $model->getRequiredFields();
250
        if (is_array($modelRequiredFields)) {
251
            foreach ($modelRequiredFields as $field) {
252
                // don't describe hidden fields
253
                if (!isset($documentFieldNames[$field])) {
254
                    continue;
255
                }
256
257
                $requiredFields[] = $documentFieldNames[$field];
258
            }
259
        }
260
261
        foreach ($meta->getFieldNames() as $field) {
262
            // don't describe hidden fields
263
            if (!isset($documentFieldNames[$field])) {
264
                continue;
265
            }
266
            // hide realId field (I was aiming at a cleaner solution than the macig realId string initially)
267
            if ($meta->getTypeOfField($field) == 'id' && $field == 'realId') {
268
                continue;
269
            }
270
271
            $property = new Schema();
272
            $property->setTitle($model->getTitleOfField($field));
273
            $property->setDescription($model->getDescriptionOfField($field));
274
275
            $property->setType($meta->getTypeOfField($field));
276
            $property->setReadOnly($model->getReadOnlyOfField($field));
277
278
            if ($meta->getTypeOfField($field) === 'many') {
279
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
280
281
                if ($model->hasDynamicKey($field)) {
282
                    $property->setType('object');
283
284
                    if ($online) {
285
                        // we generate a complete list of possible keys when we have access to mongodb
286
                        // this makes everything work with most json-schema v3 implementations (ie. schemaform.io)
287
                        $dynamicKeySpec = $model->getDynamicKeySpec($field);
288
289
                        $documentId = $dynamicKeySpec->{'document-id'};
290
                        $dynamicRepository = $this->repositoryFactory->get($documentId);
291
292
                        // put this in invalidate map so when know we have to invalidate when this document is used
293
                        $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...
294
295
                        $repositoryMethod = $dynamicKeySpec->{'repository-method'};
296
                        $records = $dynamicRepository->$repositoryMethod();
297
298
                        $dynamicProperties = array_map(
299
                            function ($record) {
300
                                return $record->getId();
301
                            },
302
                            $records
303
                        );
304
                        foreach ($dynamicProperties as $propertyName) {
305
                            $property->addProperty(
306
                                $propertyName,
307
                                $this->getModelSchema($field, $propertyModel, $online)
308
                            );
309
                        }
310
                    } else {
311
                        // swagger case
312
                        $property->setAdditionalProperties(
313
                            new SchemaAdditionalProperties($this->getModelSchema($field, $propertyModel, $online))
314
                        );
315
                    }
316
                } else {
317
                    $property->setItems($this->getModelSchema($field, $propertyModel, $online));
318
                    $property->setType('array');
319
                }
320
            } elseif ($meta->getTypeOfField($field) === 'one') {
321
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
322
                $property = $this->getModelSchema($field, $propertyModel, $online);
323
324
                if ($property->getSearchable()) {
325
                    foreach ($property->getSearchable() as $searchableSubField) {
326
                        $subSearchableFields[] = $field . '.' . $searchableSubField;
327
                    }
328
                }
329
            } elseif (in_array($field, $translatableFields, true)) {
330
                $property = $this->makeTranslatable($property, $languages);
331
            } elseif (in_array($field.'[]', $translatableFields, true)) {
332
                $property = $this->makeArrayTranslatable($property, $languages);
333
            } elseif ($meta->getTypeOfField($field) === 'extref') {
334
                $urls = array();
335
                $refCollections = $model->getRefCollectionOfField($field);
336
                foreach ($refCollections as $collection) {
337
                    if (isset($this->extrefServiceMapping[$collection])) {
338
                        $urls[] = $this->router->generate(
339
                            $this->extrefServiceMapping[$collection].'.all',
340
                            [],
341
                            UrlGeneratorInterface::ABSOLUTE_URL
342
                        );
343
                    } elseif ($collection === '*') {
344
                        $urls[] = '*';
345
                    }
346
                }
347
                $property->setRefCollection($urls);
348 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...
349
                $itemSchema = new Schema();
350
                $property->setType('array');
351
                $itemSchema->setType($this->getCollectionItemType($meta->name, $field));
352
353
                $property->setItems($itemSchema);
354
                $property->setFormat(null);
355
            } elseif ($meta->getTypeOfField($field) === 'datearray') {
356
                $itemSchema = new Schema();
357
                $property->setType('array');
358
                $itemSchema->setType('string');
359
                $itemSchema->setFormat('date-time');
360
361
                $property->setItems($itemSchema);
362
                $property->setFormat(null);
363 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...
364
                $itemSchema = new Schema();
365
                $itemSchema->setType('object');
366
367
                $property->setType('array');
368
                $property->setItems($itemSchema);
369
                $property->setFormat(null);
370
            } elseif (in_array($meta->getTypeOfField($field), $property->getMinLengthTypes())) {
371
                // make sure a required field cannot be blank
372
                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
                    $currentType = $property->getType();
377
                    if ($currentType instanceof SchemaType) {
378
                        $property->setType(array_merge($currentType->getTypes(), ['null']));
379
                    } else {
380
                        $property->setType('null');
381
                    }
382
                }
383
            }
384
385
            $property = $this->constraintBuilder->addConstraints($field, $property, $model);
386
387
            $schema->addProperty($documentFieldNames[$field], $property);
388
        }
389
390
        if ($meta->isEmbeddedDocument && !in_array('id', $model->getRequiredFields())) {
391
            $schema->removeProperty('id');
392
        }
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
        $idPosition = array_search('id', $requiredFields);
400
        if ($internal === true && $idPosition !== false) {
401
            unset($requiredFields[$idPosition]);
402
        }
403
404
        $schema->setRequired($requiredFields);
405
406
        // set additionalProperties to false (as this is our default policy) if not already set
407
        if (is_null($schema->getAdditionalProperties()) && $online) {
408
            $schema->setAdditionalProperties(new SchemaAdditionalProperties(false));
409
        }
410
411
        $searchableFields = array_merge($subSearchableFields, $model->getSearchableFields());
412
        $schema->setSearchable($searchableFields);
413
414
        $this->cache->save($cacheKey, $schema);
415
        $this->cache->save($this->cacheInvalidationMapKey, $invalidateCacheMap);
416
417
        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
    public static function getSchemaRouteName($routeName)
470
    {
471
        $routeParts = explode('.', $routeName);
472
473
        $routeType = array_pop($routeParts);
474
        // check if we need to create an item or collection schema
475
        $realRouteType = 'canonicalSchema';
476
        if ($routeType != 'options' && $routeType != 'all') {
477
            $realRouteType = 'canonicalIdSchema';
478
        }
479
480
        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
    private function getCollectionItemType($className, $fieldName)
491
    {
492
        $serializerMetadata = $this->serializerMetadataFactory->getMetadataForClass($className);
493
        if ($serializerMetadata === null) {
494
            return null;
495
        }
496
        if (!isset($serializerMetadata->propertyMetadata[$fieldName])) {
497
            return null;
498
        }
499
500
        $type = $serializerMetadata->propertyMetadata[$fieldName]->type;
501
        return isset($type['name'], $type['params'][0]['name']) && $type['name'] === 'array' ?
502
            $type['params'][0]['name'] :
503
            null;
504
    }
505
}
506