Failed Conditions
Push — feature/moar-test-optimizing ( 091eca...fb462a )
by Lucas
24:29 queued 14:31
created

SchemaUtils::makeTranslatable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2
Metric Value
dl 0
loc 18
rs 9.4285
ccs 0
cts 4
cp 0
cc 1
eloc 13
nc 1
nop 2
crap 2
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
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
        $modelRequiredFields = $model->getRequiredFields();
307
        if (is_array($modelRequiredFields)) {
308
            foreach ($modelRequiredFields as $field) {
309
                // don't describe hidden fields
310
                if (!isset($documentFieldNames[$field])) {
311
                    continue;
312
                }
313
314
                $requiredFields[] = $documentFieldNames[$field];
315
            }
316
        }
317
        $schema->setRequired($requiredFields);
318
319
        $searchableFields = array_merge($subSearchableFields, $model->getSearchableFields());
320
321
        $schema->setSearchable($searchableFields);
322
323
        return $schema;
324
    }
325
326
    /**
327
     * turn a property into a translatable property
328
     *
329
     * @param Schema   $property  simple string property
330
     * @param string[] $languages available languages
331
     *
332
     * @return Schema
333
     */
334
    public function makeTranslatable(Schema $property, $languages)
335
    {
336
        $property->setType('object');
337
        $property->setTranslatable(true);
338
339
        array_walk(
340
            $languages,
341
            function ($language) use ($property) {
342
                $schema = new Schema;
343
                $schema->setType('string');
344
                $schema->setTitle('Translated String');
345
                $schema->setDescription('String in ' . $language . ' locale.');
346
                $property->addProperty($language, $schema);
347
            }
348
        );
349
        $property->setRequired(['en']);
350
        return $property;
351
    }
352
353
    /**
354
     * turn a array property into a translatable property
355
     *
356
     * @param Schema   $property  simple string property
357
     * @param string[] $languages available languages
358 6
     *
359
     * @return Schema
360 6
     */
361
    public function makeArrayTranslatable(Schema $property, $languages)
362 6
    {
363
        $property->setType('array');
364 6
        $property->setItems($this->makeTranslatable(new Schema(), $languages));
365 6
        return $property;
366 5
    }
367 5
368
    /**
369 6
     * get canonical route to a schema based on a route
370
     *
371
     * @param string $routeName route name
372
     *
373
     * @return string schema route name
374
     */
375
    public static function getSchemaRouteName($routeName)
376
    {
377
        $routeParts = explode('.', $routeName);
378
379
        $routeType = array_pop($routeParts);
380
        // check if we need to create an item or collection schema
381
        $realRouteType = 'canonicalSchema';
382
        if ($routeType != 'options' && $routeType != 'all') {
383
            $realRouteType = 'canonicalIdSchema';
384
        }
385
386
        return implode('.', array_merge($routeParts, array($realRouteType)));
387
    }
388
389
    /**
390
     * Get item type of collection field
391
     *
392
     * @param string $className Class name
393
     * @param string $fieldName Field name
394
     * @return string|null
395
     */
396
    private function getCollectionItemType($className, $fieldName)
397
    {
398
        $serializerMetadata = $this->serializerMetadataFactory->getMetadataForClass($className);
399
        if ($serializerMetadata === null) {
400
            return null;
401
        }
402
        if (!isset($serializerMetadata->propertyMetadata[$fieldName])) {
403
            return null;
404
        }
405
406
        $type = $serializerMetadata->propertyMetadata[$fieldName]->type;
407
        return isset($type['name'], $type['params'][0]['name']) && $type['name'] === 'array' ?
408
            $type['params'][0]['name'] :
409
            null;
410
    }
411
}
412