Completed
Push — feature/EVO-4597-rabbitmq-hand... ( 6e503b...616297 )
by
unknown
52:22 queued 46:35
created

SchemaUtils::makeTranslatable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 1.0023
Metric Value
dl 0
loc 18
ccs 13
cts 15
cp 0.8667
rs 9.4285
cc 1
eloc 13
nc 1
nop 2
crap 1.0023
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 190
    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 190
        $this->repositoryFactory = $repositoryFactory;
98 190
        $this->serializerMetadataFactory = $serializerMetadataFactory;
99 190
        $this->languageRepository = $languageRepository;
100 190
        $this->router = $router;
101 190
        $this->extrefServiceMapping = $extrefServiceMapping;
102 190
        $this->eventMap = $eventMap;
103 190
        $this->documentFieldNames = $documentFieldNames;
104 190
        $this->defaultLocale = $defaultLocale;
105 190
    }
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 9
    public function getCollectionSchema($modelName, DocumentModel $model)
116
    {
117 8
        $collectionSchema = new Schema;
118 8
        $collectionSchema->setTitle(sprintf('Array of %s objects', $modelName));
119 9
        $collectionSchema->setType('array');
120
121 8
        $collectionSchema->setItems($this->getModelSchema($modelName, $model));
122
123 8
        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 96
    public function getModelSchema($modelName, DocumentModel $model, $online = true)
136
    {
137
        // build up schema data
138 96
        $schema = new Schema;
139
140 96
        if (!empty($model->getTitle())) {
141 25
            $schema->setTitle($model->getTitle());
142
        } else {
143 73
            $schema->setTitle(ucfirst($modelName));
144
        }
145
146 96
        $schema->setDescription($model->getDescription());
147 96
        $schema->setType('object');
148
149
        // grab schema info from model
150 96
        $repo = $model->getRepository();
151 96
        $meta = $repo->getClassMetadata();
152
153
        // Init sub searchable fields
154 96
        $subSearchableFields = array();
155
156
        // look for translatables in document class
157 96
        $documentReflection = new \ReflectionClass($repo->getClassName());
158 96
        if ($documentReflection->implementsInterface('Graviton\I18nBundle\Document\TranslatableDocumentInterface')) {
159
            /** @var TranslatableDocumentInterface $documentInstance */
160 95
            $documentInstance = $documentReflection->newInstanceWithoutConstructor();
161 95
            $translatableFields = array_merge(
162 95
                $documentInstance->getTranslatableFields(),
163 95
                $documentInstance->getPreTranslatedFields()
164 2
            );
165 2
        } else {
166 2
            $translatableFields = [];
167
        }
168
169
        // exposed fields
170 96
        $documentFieldNames = isset($this->documentFieldNames[$repo->getClassName()]) ?
171 96
            $this->documentFieldNames[$repo->getClassName()] :
172 96
            [];
173
174 96
        if ($online) {
175 96
            $languages = array_map(
176
                function (Language $language) {
177 56
                    return $language->getId();
178 96
                },
179 96
                $this->languageRepository->findAll()
180 2
            );
181 2
        } else {
182
            $languages = [
183
                $this->defaultLocale
184
            ];
185
        }
186
187
        // exposed events..
188 96
        $classShortName = $documentReflection->getShortName();
189 96
        if (isset($this->eventMap[$classShortName])) {
190 96
            $schema->setEventNames(array_unique($this->eventMap[$classShortName]['events']));
191 2
        }
192
193 96
        foreach ($meta->getFieldNames() as $field) {
194
            // don't describe hidden fields
195 96
            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 96
            if ($meta->getTypeOfField($field) == 'id' && $field == 'realId') {
200 1
                continue;
201
            }
202
203 96
            $property = new Schema();
204 96
            $property->setTitle($model->getTitleOfField($field));
205 96
            $property->setDescription($model->getDescriptionOfField($field));
206
207 96
            $property->setType($meta->getTypeOfField($field));
208 96
            $property->setReadOnly($model->getReadOnlyOfField($field));
209
210 96
            if ($meta->getTypeOfField($field) === 'many') {
211 65
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
212
213 65
                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 65
                    $property->setItems($this->getModelSchema($field, $propertyModel, $online));
245 65
                    $property->setType('array');
246
                }
247 96
            } elseif ($meta->getTypeOfField($field) === 'one') {
248 73
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
249 73
                $property = $this->getModelSchema($field, $propertyModel, $online);
250
251 73
                if ($property->getSearchable()) {
252
                    foreach ($property->getSearchable() as $searchableSubField) {
253 73
                        $subSearchableFields[] = $field . '.' . $searchableSubField;
254
                    }
255
                }
256 96
            } elseif (in_array($field, $translatableFields, true)) {
257 57
                $property = $this->makeTranslatable($property, $languages);
258 96
            } elseif (in_array($field.'[]', $translatableFields, true)) {
259 3
                $property = $this->makeArrayTranslatable($property, $languages);
260 96
            } elseif ($meta->getTypeOfField($field) === 'extref') {
261 61
                $urls = array();
262 61
                $refCollections = $model->getRefCollectionOfField($field);
263 61
                foreach ($refCollections as $collection) {
264 36
                    if (isset($this->extrefServiceMapping[$collection])) {
265 34
                        $urls[] = $this->router->generate(
266 34
                            $this->extrefServiceMapping[$collection].'.all',
267 34
                            [],
268 34
                            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 2
                    } elseif ($collection === '*') {
271 36
                        $urls[] = '*';
272
                    }
273
                }
274 61
                $property->setRefCollection($urls);
275 96 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 3
                $itemSchema = new Schema();
277 3
                $property->setType('array');
278 3
                $itemSchema->setType($this->getCollectionItemType($meta->name, $field));
279
280 3
                $property->setItems($itemSchema);
281 3
                $property->setFormat(null);
282 96
            } elseif ($meta->getTypeOfField($field) === 'datearray') {
283 3
                $itemSchema = new Schema();
284 3
                $property->setType('array');
285 3
                $itemSchema->setType('string');
286 3
                $itemSchema->setFormat('date-time');
287
288 3
                $property->setItems($itemSchema);
289 3
                $property->setFormat(null);
290 96 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 3
                $itemSchema = new Schema();
292 3
                $itemSchema->setType('object');
293
294 3
                $property->setType('array');
295 3
                $property->setItems($itemSchema);
296 3
                $property->setFormat(null);
297
            }
298 96
            $schema->addProperty($documentFieldNames[$field], $property);
299 2
        }
300
301 96
        if ($meta->isEmbeddedDocument && !in_array('id', $model->getRequiredFields())) {
302 73
            $schema->removeProperty('id');
303
        }
304
305 96
        $requiredFields = [];
306 96
        $modelRequiredFields = $model->getRequiredFields();
307 96
        if (is_array($modelRequiredFields)) {
308 96
            foreach ($modelRequiredFields as $field) {
309
                // don't describe hidden fields
310 72
                if (!isset($documentFieldNames[$field])) {
311
                    continue;
312
                }
313
314 72
                $requiredFields[] = $documentFieldNames[$field];
315 2
            }
316 2
        }
317 96
        $schema->setRequired($requiredFields);
318
319 96
        $searchableFields = array_merge($subSearchableFields, $model->getSearchableFields());
320
321 96
        $schema->setSearchable($searchableFields);
322
323 96
        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 57
    public function makeTranslatable(Schema $property, $languages)
335
    {
336 57
        $property->setType('object');
337 57
        $property->setTranslatable(true);
338
339 57
        array_walk(
340
            $languages,
341 57
            function ($language) use ($property) {
342 55
                $schema = new Schema;
343 55
                $schema->setType('string');
344 55
                $schema->setTitle('Translated String');
345 55
                $schema->setDescription('String in ' . $language . ' locale.');
346 55
                $property->addProperty($language, $schema);
347 57
            }
348
        );
349 57
        $property->setRequired(['en']);
350 57
        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
     *
359
     * @return Schema
360
     */
361 3
    public function makeArrayTranslatable(Schema $property, $languages)
362
    {
363 3
        $property->setType('array');
364 3
        $property->setItems($this->makeTranslatable(new Schema(), $languages));
365 3
        return $property;
366
    }
367
368
    /**
369
     * 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 203
    public static function getSchemaRouteName($routeName)
376
    {
377 203
        $routeParts = explode('.', $routeName);
378
379 203
        $routeType = array_pop($routeParts);
380
        // check if we need to create an item or collection schema
381 203
        $realRouteType = 'canonicalSchema';
382 203
        if ($routeType != 'options' && $routeType != 'all') {
383 113
            $realRouteType = 'canonicalIdSchema';
384 6
        }
385
386 203
        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 3
    private function getCollectionItemType($className, $fieldName)
397
    {
398 3
        $serializerMetadata = $this->serializerMetadataFactory->getMetadataForClass($className);
399 3
        if ($serializerMetadata === null) {
400
            return null;
401
        }
402 3
        if (!isset($serializerMetadata->propertyMetadata[$fieldName])) {
403
            return null;
404
        }
405
406 3
        $type = $serializerMetadata->propertyMetadata[$fieldName]->type;
407 3
        return isset($type['name'], $type['params'][0]['name']) && $type['name'] === 'array' ?
408 3
            $type['params'][0]['name'] :
409 3
            null;
410
    }
411
}
412