Completed
Push — feature/other-validation ( 01c6ab...a6eaef )
by Narcotic
09:55
created

SchemaUtils::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 23
rs 9.0856
ccs 12
cts 12
cp 1
cc 1
eloc 21
nc 1
nop 10
crap 1

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