Failed Conditions
Push — feature/EVO-4410-username-id-t... ( fedcd7...832965 )
by
unknown
10:33
created

SchemaUtils::getModelSchema()   F

Complexity

Conditions 30
Paths 2880

Size

Total Lines 187
Code Lines 117

Duplication

Lines 16
Ratio 8.56 %

Importance

Changes 4
Bugs 0 Features 1
Metric Value
c 4
b 0
f 1
dl 16
loc 187
rs 2
cc 30
eloc 117
nc 2880
nop 3

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
17
/**
18
 * Utils for generating schemas.
19
 *
20
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
21
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
22
 * @link     http://swisscom.ch
23
 */
24
class SchemaUtils
25
{
26
27
    /**
28
     * language repository
29
     *
30
     * @var LanguageRepository repository
31
     */
32
    private $languageRepository;
33
34
    /**
35
     * router
36
     *
37
     * @var RouterInterface router
38
     */
39
    private $router;
40
41
    /**
42
     * mapping service names => route names
43
     *
44
     * @var array service mapping
45
     */
46
    private $extrefServiceMapping;
47
48
    /**
49
     * event map
50
     *
51
     * @var array event map
52
     */
53
    private $eventMap;
54
55
    /**
56
     * @var array [document class => [field name -> exposed name]]
57
     */
58
    private $documentFieldNames;
59
60
    /**
61
     * @var string
62
     */
63
    private $defaultLocale;
64
65
    /**
66
     * @var RepositoryFactory
67
     */
68
    private $repositoryFactory;
69
70
    /**
71
     * @var SerializerMetadataFactoryInterface
72
     */
73
    private $serializerMetadataFactory;
74
75
    /**
76
     * Constructor
77
     *
78
     * @param RepositoryFactory                  $repositoryFactory         Create repos from model class names
79
     * @param SerializerMetadataFactoryInterface $serializerMetadataFactory Serializer metadata factory
80
     * @param LanguageRepository                 $languageRepository        repository
81
     * @param RouterInterface                    $router                    router
82
     * @param array                              $extrefServiceMapping      Extref service mapping
83
     * @param array                              $eventMap                  eventmap
84
     * @param array                              $documentFieldNames        Document field names
85
     * @param string                             $defaultLocale             Default Language
86
     */
87
    public function __construct(
88
        RepositoryFactory $repositoryFactory,
89
        SerializerMetadataFactoryInterface $serializerMetadataFactory,
90
        LanguageRepository $languageRepository,
91
        RouterInterface $router,
92
        array $extrefServiceMapping,
93
        array $eventMap,
94
        array $documentFieldNames,
95
        $defaultLocale
96
    ) {
97
        $this->repositoryFactory = $repositoryFactory;
98
        $this->serializerMetadataFactory = $serializerMetadataFactory;
99
        $this->languageRepository = $languageRepository;
100
        $this->router = $router;
101
        $this->extrefServiceMapping = $extrefServiceMapping;
102
        $this->eventMap = $eventMap;
103
        $this->documentFieldNames = $documentFieldNames;
104
        $this->defaultLocale = $defaultLocale;
105
    }
106
107
    /**
108
     * get schema for an array of models
109
     *
110
     * @param string        $modelName name of model
111
     * @param DocumentModel $model     model
112
     *
113
     * @return Schema
114
     */
115
    public function getCollectionSchema($modelName, DocumentModel $model)
116
    {
117
        $collectionSchema = new Schema;
118
        $collectionSchema->setTitle(sprintf('Array of %s objects', $modelName));
119
        $collectionSchema->setType('array');
120
121
        $collectionSchema->setItems($this->getModelSchema($modelName, $model));
122
123
        return $collectionSchema;
124
    }
125
126
    /**
127
     * return the schema for a given route
128
     *
129
     * @param string        $modelName name of mode to generate schema for
130
     * @param DocumentModel $model     model to generate schema for
131
     * @param boolean       $online    if we are online and have access to mongodb during this build
132
     *
133
     * @return Schema
134
     */
135
    public function getModelSchema($modelName, DocumentModel $model, $online = true)
136
    {
137
        // build up schema data
138
        $schema = new Schema;
139
140
        if (!empty($model->getTitle())) {
141
            $schema->setTitle($model->getTitle());
142
        } else {
143
            $schema->setTitle(ucfirst($modelName));
144
        }
145
146
        $schema->setDescription($model->getDescription());
147
        $schema->setType('object');
148
149
        // grab schema info from model
150
        $repo = $model->getRepository();
151
        $meta = $repo->getClassMetadata();
152
153
        // Init sub searchable fields
154
        $subSearchableFields = array();
155
156
        // look for translatables in document class
157
        $documentReflection = new \ReflectionClass($repo->getClassName());
158
        if ($documentReflection->implementsInterface('Graviton\I18nBundle\Document\TranslatableDocumentInterface')) {
159
            /** @var TranslatableDocumentInterface $documentInstance */
160
            $documentInstance = $documentReflection->newInstanceWithoutConstructor();
161
            $translatableFields = array_merge(
162
                $documentInstance->getTranslatableFields(),
163
                $documentInstance->getPreTranslatedFields()
164
            );
165
        } else {
166
            $translatableFields = [];
167
        }
168
169
        // exposed fields
170
        $documentFieldNames = isset($this->documentFieldNames[$repo->getClassName()]) ?
171
            $this->documentFieldNames[$repo->getClassName()] :
172
            [];
173
174
        if ($online) {
175
            $languages = array_map(
176
                function (Language $language) {
177
                    return $language->getId();
178
                },
179
                $this->languageRepository->findAll()
180
            );
181
        } else {
182
            $languages = [
183
                $this->defaultLocale
184
            ];
185
        }
186
187
        // exposed events..
188
        $classShortName = $documentReflection->getShortName();
189
        if (isset($this->eventMap[$classShortName])) {
190
            $schema->setEventNames(array_unique($this->eventMap[$classShortName]['events']));
191
        }
192
193
        foreach ($meta->getFieldNames() as $field) {
194
            // don't describe hidden fields
195
            if (!isset($documentFieldNames[$field])) {
196
                continue;
197
            }
198
            // hide realId field (I was aiming at a cleaner solution than the macig realId string initially)
199
            if ($meta->getTypeOfField($field) == 'id' && $field == 'realId') {
200
                continue;
201
            }
202
203
            $property = new Schema();
204
            $property->setTitle($model->getTitleOfField($field));
205
            $property->setDescription($model->getDescriptionOfField($field));
206
207
            $property->setType($meta->getTypeOfField($field));
208
            $property->setReadOnly($model->getReadOnlyOfField($field));
209
210
            if ($meta->getTypeOfField($field) === 'many') {
211
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
212
213
                if ($model->hasDynamicKey($field)) {
214
                    $property->setType('object');
215
216
                    if ($online) {
217
                        // we generate a complete list of possible keys when we have access to mongodb
218
                        // this makes everything work with most json-schema v3 implementations (ie. schemaform.io)
219
                        $dynamicKeySpec = $model->getDynamicKeySpec($field);
220
221
                        $documentId = $dynamicKeySpec->{'document-id'};
222
                        $dynamicRepository = $this->repositoryFactory->get($documentId);
223
224
                        $repositoryMethod = $dynamicKeySpec->{'repository-method'};
225
                        $records = $dynamicRepository->$repositoryMethod();
226
227
                        $dynamicProperties = array_map(
228
                            function ($record) {
229
                                return $record->getId();
230
                            },
231
                            $records
232
                        );
233
                        foreach ($dynamicProperties as $propertyName) {
234
                            $property->addProperty(
235
                                $propertyName,
236
                                $this->getModelSchema($field, $propertyModel, $online)
237
                            );
238
                        }
239
                    } else {
240
                        // in the swagger case we can use additionPorerties which where introduced by json-schema v4
241
                        $property->setAdditionalProperties($this->getModelSchema($field, $propertyModel, $online));
242
                    }
243
                } else {
244
                    $property->setItems($this->getModelSchema($field, $propertyModel, $online));
245
                    $property->setType('array');
246
                }
247
            } elseif ($meta->getTypeOfField($field) === 'one') {
248
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
249
                $property = $this->getModelSchema($field, $propertyModel, $online);
250
251
                if ($property->getSearchable()) {
252
                    foreach ($property->getSearchable() as $searchableSubField) {
253
                        $subSearchableFields[] = $field . '.' . $searchableSubField;
254
                    }
255
                }
256
            } elseif (in_array($field, $translatableFields, true)) {
257
                $property = $this->makeTranslatable($property, $languages);
258
            } elseif (in_array($field.'[]', $translatableFields, true)) {
259
                $property = $this->makeArrayTranslatable($property, $languages);
260
            } elseif ($meta->getTypeOfField($field) === 'extref') {
261
                $urls = array();
262
                $refCollections = $model->getRefCollectionOfField($field);
263
                foreach ($refCollections as $collection) {
264
                    if (isset($this->extrefServiceMapping[$collection])) {
265
                        $urls[] = $this->router->generate(
266
                            $this->extrefServiceMapping[$collection].'.all',
267
                            [],
268
                            true
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
269
                        );
270
                    } elseif ($collection === '*') {
271
                        $urls[] = '*';
272
                    }
273
                }
274
                $property->setRefCollection($urls);
275 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...
276
                $itemSchema = new Schema();
277
                $property->setType('array');
278
                $itemSchema->setType($this->getCollectionItemType($meta->name, $field));
279
280
                $property->setItems($itemSchema);
281
                $property->setFormat(null);
282
            } elseif ($meta->getTypeOfField($field) === 'datearray') {
283
                $itemSchema = new Schema();
284
                $property->setType('array');
285
                $itemSchema->setType('string');
286
                $itemSchema->setFormat('date-time');
287
288
                $property->setItems($itemSchema);
289
                $property->setFormat(null);
290 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...
291
                $itemSchema = new Schema();
292
                $itemSchema->setType('object');
293
294
                $property->setType('array');
295
                $property->setItems($itemSchema);
296
                $property->setFormat(null);
297
            }
298
            $schema->addProperty($documentFieldNames[$field], $property);
299
        }
300
301
        if ($meta->isEmbeddedDocument && !in_array('id', $model->getRequiredFields())) {
302
            $schema->removeProperty('id');
303
        }
304
305
        $requiredFields = [];
306
        foreach ($model->getRequiredFields() as $field) {
307
            // don't describe hidden fields
308
            if (!isset($documentFieldNames[$field])) {
309
                continue;
310
            }
311
312
            $requiredFields[] = $documentFieldNames[$field];
313
        }
314
        $schema->setRequired($requiredFields);
315
316
        $searchableFields = array_merge($subSearchableFields, $model->getSearchableFields());
317
318
        $schema->setSearchable($searchableFields);
319
320
        return $schema;
321
    }
322
323
    /**
324
     * turn a property into a translatable property
325
     *
326
     * @param Schema   $property  simple string property
327
     * @param string[] $languages available languages
328
     *
329
     * @return Schema
330
     */
331
    public function makeTranslatable(Schema $property, $languages)
332
    {
333
        $property->setType('object');
334
        $property->setTranslatable(true);
335
336
        array_walk(
337
            $languages,
338
            function ($language) use ($property) {
339
                $schema = new Schema;
340
                $schema->setType('string');
341
                $schema->setTitle('Translated String');
342
                $schema->setDescription('String in ' . $language . ' locale.');
343
                $property->addProperty($language, $schema);
344
            }
345
        );
346
        $property->setRequired(['en']);
347
        return $property;
348
    }
349
350
    /**
351
     * turn a array property into a translatable property
352
     *
353
     * @param Schema   $property  simple string property
354
     * @param string[] $languages available languages
355
     *
356
     * @return Schema
357
     */
358
    public function makeArrayTranslatable(Schema $property, $languages)
359
    {
360
        $property->setType('array');
361
        $property->setItems($this->makeTranslatable(new Schema(), $languages));
362
        return $property;
363
    }
364
365
    /**
366
     * get canonical route to a schema based on a route
367
     *
368
     * @param string $routeName route name
369
     *
370
     * @return string schema route name
371
     */
372
    public static function getSchemaRouteName($routeName)
373
    {
374
        $routeParts = explode('.', $routeName);
375
376
        $routeType = array_pop($routeParts);
377
        // check if we need to create an item or collection schema
378
        $realRouteType = 'canonicalSchema';
379
        if ($routeType != 'options' && $routeType != 'all') {
380
            $realRouteType = 'canonicalIdSchema';
381
        }
382
383
        return implode('.', array_merge($routeParts, array($realRouteType)));
384
    }
385
386
    /**
387
     * Get item type of collection field
388
     *
389
     * @param string $className Class name
390
     * @param string $fieldName Field name
391
     * @return string|null
392
     */
393
    private function getCollectionItemType($className, $fieldName)
394
    {
395
        $serializerMetadata = $this->serializerMetadataFactory->getMetadataForClass($className);
396
        if ($serializerMetadata === null) {
397
            return null;
398
        }
399
        if (!isset($serializerMetadata->propertyMetadata[$fieldName])) {
400
            return null;
401
        }
402
403
        $type = $serializerMetadata->propertyMetadata[$fieldName]->type;
404
        return isset($type['name'], $type['params'][0]['name']) && $type['name'] === 'array' ?
405
            $type['params'][0]['name'] :
406
            null;
407
    }
408
}
409