Completed
Pull Request — develop (#395)
by
unknown
17:46 queued 10:45
created

SchemaUtils::getSchemaRouteName()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3
Metric Value
dl 0
loc 13
ccs 7
cts 7
cp 1
rs 9.4285
cc 3
eloc 7
nc 2
nop 1
crap 3
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 2
    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 2
        $this->repositoryFactory = $repositoryFactory;
98 2
        $this->serializerMetadataFactory = $serializerMetadataFactory;
99 2
        $this->languageRepository = $languageRepository;
100 2
        $this->router = $router;
101 2
        $this->extrefServiceMapping = $extrefServiceMapping;
102 2
        $this->eventMap = $eventMap;
103 2
        $this->documentFieldNames = $documentFieldNames;
104 2
        $this->defaultLocale = $defaultLocale;
105 2
    }
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
        $collectionSchema->setItems($this->getModelSchema($modelName, $model));
121
122
        return $collectionSchema;
123
    }
124
125
    /**
126
     * return the schema for a given route
127
     *
128
     * @param string        $modelName name of mode to generate schema for
129
     * @param DocumentModel $model     model to generate schema for
130
     * @param boolean       $online    if we are online and have access to mongodb during this build
131
     *
132
     * @return Schema
133
     */
134
    public function getModelSchema($modelName, DocumentModel $model, $online = true)
135
    {
136
        // build up schema data
137
        $schema = new Schema;
138
139
        if (!empty($model->getTitle())) {
140
            $schema->setTitle($model->getTitle());
141
        } else {
142
            $schema->setTitle(ucfirst($modelName));
143
        }
144
        
145
        $schema->setDescription($model->getDescription());
146
        $schema->setType('object');
147
148
        // grab schema info from model
149
        $repo = $model->getRepository();
150
151
        if (!$repo) {
152
            return $schema;
153
        }
154
        $meta = $repo->getClassMetadata();
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
            } elseif (in_array($field, $translatableFields, true)) {
251
                $property = $this->makeTranslatable($property, $languages);
252
            } elseif (in_array($field.'[]', $translatableFields, true)) {
253
                $property = $this->makeArrayTranslatable($property, $languages);
254
            } elseif ($meta->getTypeOfField($field) === 'extref') {
255
                $urls = array();
256
                $refCollections = $model->getRefCollectionOfField($field);
257
                foreach ($refCollections as $collection) {
258
                    if (isset($this->extrefServiceMapping[$collection])) {
259
                        $urls[] = $this->router->generate(
260
                            $this->extrefServiceMapping[$collection].'.all',
261
                            [],
262
                            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...
263
                        );
264
                    } elseif ($collection === '*') {
265
                        $urls[] = '*';
266
                    }
267
                }
268
                $property->setRefCollection($urls);
269 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...
270
                $itemSchema = new Schema();
271
                $property->setType('array');
272
                $itemSchema->setType($this->getCollectionItemType($meta->name, $field));
273
274
                $property->setItems($itemSchema);
275
                $property->setFormat(null);
276
            } elseif ($meta->getTypeOfField($field) === 'datearray') {
277
                $itemSchema = new Schema();
278
                $property->setType('array');
279
                $itemSchema->setType('string');
280
                $itemSchema->setFormat('date-time');
281
282
                $property->setItems($itemSchema);
283
                $property->setFormat(null);
284 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...
285
                $itemSchema = new Schema();
286
                $itemSchema->setType('object');
287
288
                $property->setType('array');
289
                $property->setItems($itemSchema);
290
                $property->setFormat(null);
291
            }
292
            $schema->addProperty($documentFieldNames[$field], $property);
293
        }
294
295
        if ($meta->isEmbeddedDocument && !in_array('id', $model->getRequiredFields())) {
296
            $schema->removeProperty('id');
297
        }
298
299
        $requiredFields = [];
300
        foreach ($model->getRequiredFields() as $field) {
301
            // don't describe hidden fields
302
            if (!isset($documentFieldNames[$field])) {
303
                continue;
304
            }
305
306
            $requiredFields[] = $documentFieldNames[$field];
307
        }
308
        $schema->setRequired($requiredFields);
309
310
        return $schema;
311
    }
312
313
    /**
314
     * turn a property into a translatable property
315
     *
316
     * @param Schema   $property  simple string property
317
     * @param string[] $languages available languages
318
     *
319
     * @return Schema
320
     */
321
    public function makeTranslatable(Schema $property, $languages)
322
    {
323
        $property->setType('object');
324
        $property->setTranslatable(true);
325
326
        array_walk(
327
            $languages,
328
            function ($language) use ($property) {
329
                $schema = new Schema;
330
                $schema->setType('string');
331
                $schema->setTitle('Translated String');
332
                $schema->setDescription('String in ' . $language . ' locale.');
333
                $property->addProperty($language, $schema);
334
            }
335
        );
336
        $property->setRequired(['en']);
337
        return $property;
338
    }
339
340
    /**
341
     * turn a array property into a translatable property
342
     *
343
     * @param Schema   $property  simple string property
344
     * @param string[] $languages available languages
345
     *
346
     * @return Schema
347
     */
348
    public function makeArrayTranslatable(Schema $property, $languages)
349
    {
350
        $property->setType('array');
351
        $property->setItems($this->makeTranslatable(new Schema(), $languages));
352
        return $property;
353
    }
354
355
    /**
356
     * get canonical route to a schema based on a route
357
     *
358
     * @param string $routeName route name
359
     *
360
     * @return string schema route name
361
     */
362 6
    public static function getSchemaRouteName($routeName)
363
    {
364 6
        $routeParts = explode('.', $routeName);
365
366 6
        $routeType = array_pop($routeParts);
367
        // check if we need to create an item or collection schema
368 6
        $realRouteType = 'canonicalSchema';
369 6
        if ($routeType != 'options' && $routeType != 'all') {
370 5
            $realRouteType = 'canonicalIdSchema';
371
        }
372
373 6
        return implode('.', array_merge($routeParts, array($realRouteType)));
374
    }
375
376
    /**
377
     * Get item type of collection field
378
     *
379
     * @param string $className Class name
380
     * @param string $fieldName Field name
381
     * @return string|null
382
     */
383
    private function getCollectionItemType($className, $fieldName)
384
    {
385
        $serializerMetadata = $this->serializerMetadataFactory->getMetadataForClass($className);
386
        if ($serializerMetadata === null) {
387
            return null;
388
        }
389
        if (!isset($serializerMetadata->propertyMetadata[$fieldName])) {
390
            return null;
391
        }
392
393
        $type = $serializerMetadata->propertyMetadata[$fieldName]->type;
394
        return isset($type['name'], $type['params'][0]['name']) && $type['name'] === 'array' ?
395
            $type['params'][0]['name'] :
396
            null;
397
    }
398
}
399