Completed
Push — develop ( fe1513...ed9012 )
by
unknown
13s
created

SchemaUtils   D

Complexity

Total Complexity 61

Size/Duplication

Total Lines 517
Duplicated Lines 3.09 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 6.51%

Importance

Changes 0
Metric Value
wmc 61
lcom 1
cbo 15
dl 16
loc 517
ccs 14
cts 215
cp 0.0651
rs 4.054
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 27 1
A getCollectionSchema() 0 10 1
F getModelSchema() 16 283 49
A makeTranslatable() 0 18 1
A makeArrayTranslatable() 0 6 1
A getSchemaRouteName() 0 13 3
B getCollectionItemType() 0 15 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SchemaUtils often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchemaUtils, and based on these observations, apply Extract Interface, too.

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\Document\SchemaType;
17
use Graviton\SchemaBundle\Service\RepositoryFactory;
18
use JMS\Serializer\Serializer;
19
use Metadata\MetadataFactoryInterface as SerializerMetadataFactoryInterface;
20
use Symfony\Component\Routing\RouterInterface;
21
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
22
23
/**
24
 * Utils for generating schemas.
25
 *
26
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
27
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
28
 * @link     http://swisscom.ch
29
 */
30
class SchemaUtils
31
{
32
33
    /**
34
     * language repository
35
     *
36
     * @var LanguageRepository repository
37
     */
38
    private $languageRepository;
39
40
    /**
41
     * router
42
     *
43
     * @var RouterInterface router
44
     */
45
    private $router;
46
47
    /**
48
     * serializer
49
     *
50
     * @var Serializer
51
     */
52
    private $serializer;
53
54
    /**
55
     * mapping service names => route names
56
     *
57
     * @var array service mapping
58
     */
59
    private $extrefServiceMapping;
60
61
    /**
62
     * event map
63
     *
64
     * @var array event map
65
     */
66
    private $eventMap;
67
68
    /**
69
     * @var array [document class => [field name -> exposed name]]
70
     */
71
    private $documentFieldNames;
72
73
    /**
74
     * @var string
75
     */
76
    private $defaultLocale;
77
78
    /**
79
     * @var RepositoryFactory
80
     */
81
    private $repositoryFactory;
82
83
    /**
84
     * @var SerializerMetadataFactoryInterface
85
     */
86
    private $serializerMetadataFactory;
87
88
    /**
89
     * @var CacheProvider
90
     */
91
    private $cache;
92
93
    /**
94
     * @var string
95
     */
96
    private $cacheInvalidationMapKey;
97
98
    /**
99
     * @var ConstraintBuilder
100
     */
101
    private $constraintBuilder;
102
103
    /**
104
     * Constructor
105
     *
106
     * @param RepositoryFactory                  $repositoryFactory         Create repos from model class names
107
     * @param SerializerMetadataFactoryInterface $serializerMetadataFactory Serializer metadata factory
108
     * @param LanguageRepository                 $languageRepository        repository
109
     * @param RouterInterface                    $router                    router
110
     * @param Serializer                         $serializer                serializer
111
     * @param array                              $extrefServiceMapping      Extref service mapping
112
     * @param array                              $eventMap                  eventmap
113
     * @param array                              $documentFieldNames        Document field names
114
     * @param string                             $defaultLocale             Default Language
115
     * @param ConstraintBuilder                  $constraintBuilder         Constraint builder
116
     * @param CacheProvider                      $cache                     Doctrine cache provider
117
     * @param string                             $cacheInvalidationMapKey   Cache invalidation map cache key
118
     */
119 2
    public function __construct(
120
        RepositoryFactory $repositoryFactory,
121
        SerializerMetadataFactoryInterface $serializerMetadataFactory,
122
        LanguageRepository $languageRepository,
123
        RouterInterface $router,
124
        Serializer $serializer,
125
        array $extrefServiceMapping,
126
        array $eventMap,
127
        array $documentFieldNames,
128
        $defaultLocale,
129
        ConstraintBuilder $constraintBuilder,
130
        CacheProvider $cache,
131
        $cacheInvalidationMapKey
132
    ) {
133 2
        $this->repositoryFactory = $repositoryFactory;
134 2
        $this->serializerMetadataFactory = $serializerMetadataFactory;
135 2
        $this->languageRepository = $languageRepository;
136 2
        $this->router = $router;
137 2
        $this->serializer = $serializer;
138 2
        $this->extrefServiceMapping = $extrefServiceMapping;
139 2
        $this->eventMap = $eventMap;
140 2
        $this->documentFieldNames = $documentFieldNames;
141 2
        $this->defaultLocale = $defaultLocale;
142 2
        $this->constraintBuilder = $constraintBuilder;
143 2
        $this->cache = $cache;
144 2
        $this->cacheInvalidationMapKey = $cacheInvalidationMapKey;
145 2
    }
146
147
    /**
148
     * get schema for an array of models
149
     *
150
     * @param string        $modelName name of model
151
     * @param DocumentModel $model     model
152
     *
153
     * @return Schema
154
     */
155
    public function getCollectionSchema($modelName, DocumentModel $model)
156
    {
157
        $collectionSchema = new Schema;
158
        $collectionSchema->setTitle(sprintf('Array of %s objects', $modelName));
159
        $collectionSchema->setType('array');
160
161
        $collectionSchema->setItems($this->getModelSchema($modelName, $model));
162
163
        return $collectionSchema;
164
    }
165
166
    /**
167
     * return the schema for a given route
168
     *
169
     * @param string        $modelName  name of mode to generate schema for
170
     * @param DocumentModel $model      model to generate schema for
171
     * @param boolean       $online     if we are online and have access to mongodb during this build
172
     * @param boolean       $internal   if true, we generate the schema for internal validation use
173
     * @param boolean       $serialized if true, it will serialize the Schema object and return a \stdClass instead
174
     *
175
     * @return Schema|\stdClass Either a Schema instance or serialized as \stdClass if $serialized is true
176
     */
177
    public function getModelSchema(
178
        $modelName,
179
        DocumentModel $model,
180
        $online = true,
181
        $internal = false,
182
        $serialized = false
183
    ) {
184
185
        $cacheKey = sprintf(
186
            'schema.%s.%s.%s.%s',
187
            $model->getEntityClass(),
188
            (string) $online,
189
            (string) $internal,
190
            (string) $serialized
191
        );
192
193
        if ($this->cache->contains($cacheKey)) {
194
            return $this->cache->fetch($cacheKey);
195
        }
196
197
        $invalidateCacheMap = [];
198
        if ($this->cache->contains($this->cacheInvalidationMapKey)) {
199
            $invalidateCacheMap = $this->cache->fetch($this->cacheInvalidationMapKey);
200
        }
201
202
        // build up schema data
203
        $schema = new Schema;
204
205
        if (!empty($model->getTitle())) {
206
            $schema->setTitle($model->getTitle());
207
        } else {
208
            if (!is_null($modelName)) {
209
                $schema->setTitle(ucfirst($modelName));
210
            } else {
211
                $reflection = new \ReflectionClass($model);
212
                $schema->setTitle(ucfirst($reflection->getShortName()));
213
            }
214
        }
215
216
        $schema->setDescription($model->getDescription());
217
        $schema->setDocumentClass($model->getDocumentClass());
0 ignored issues
show
Security Bug introduced by
It seems like $model->getDocumentClass() targeting Graviton\SchemaBundle\Mo...del::getDocumentClass() can also be of type false; however, Graviton\SchemaBundle\Do...ema::setDocumentClass() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
218
        $schema->setRecordOriginModifiable($model->getRecordOriginModifiable());
219
        $schema->setIsVersioning($model->isVersioning());
220
        $schema->setType('object');
221
222
        // grab schema info from model
223
        $repo = $model->getRepository();
224
        $meta = $repo->getClassMetadata();
225
226
        // Init sub searchable fields
227
        $subSearchableFields = array();
228
229
        // look for translatables in document class
230
        $documentReflection = new \ReflectionClass($repo->getClassName());
231
        $documentClass = $documentReflection->newInstance();
232
        if ($documentReflection->implementsInterface('Graviton\I18nBundle\Document\TranslatableDocumentInterface')) {
233
            /** @var TranslatableDocumentInterface $documentInstance */
234
            $documentInstance = $documentReflection->newInstanceWithoutConstructor();
235
            $translatableFields = array_merge(
236
                $documentInstance->getTranslatableFields(),
237
                $documentInstance->getPreTranslatedFields()
238
            );
239
        } else {
240
            $translatableFields = [];
241
        }
242
243
        if (!empty($translatableFields)) {
244
            $invalidateCacheMap[$this->languageRepository->getClassName()][] = $cacheKey;
245
        }
246
247
        // exposed fields
248
        $documentFieldNames = isset($this->documentFieldNames[$repo->getClassName()]) ?
249
            $this->documentFieldNames[$repo->getClassName()] :
250
            [];
251
252
        $languages = [];
253
        if ($online) {
254
            $languages = array_map(
255
                function (Language $language) {
256
                    return $language->getId();
257
                },
258
                $this->languageRepository->findAll()
259
            );
260
        }
261
        if (empty($languages)) {
262
            $languages = [
263
                $this->defaultLocale
264
            ];
265
        }
266
267
        // exposed events..
268
        $classShortName = $documentReflection->getShortName();
269
        if (isset($this->eventMap[$classShortName])) {
270
            $schema->setEventNames(array_unique($this->eventMap[$classShortName]['events']));
271
        }
272
273
        // don't describe hidden fields
274
        $requiredFields = $model->getRequiredFields();
275
        if (empty($requiredFields) || !is_array($requiredFields)) {
276
            $requiredFields = [];
277
        }
278
        $requiredFields = array_intersect_key($documentFieldNames, array_combine($requiredFields, $requiredFields));
279
280
        foreach ($meta->getFieldNames() as $field) {
281
            // don't describe hidden fields
282
            if (!isset($documentFieldNames[$field])) {
283
                continue;
284
            }
285
286
            $isEmptyExtref = false;
287
            if (is_callable([$documentClass, 'isEmptyExtRefObject'])) {
288
                $isEmptyExtref = $documentClass->isEmptyExtRefObject();
289
            }
290
291
            // hide realId field (I was aiming at a cleaner solution than the matching realId string initially)
292
            // hide embedded ID field unless it's required or for internal validation need, back-compatibility.
293
            // TODO remove !$internal once no clients use it for embedded objects as these id are done automatically
294
            if (($meta->getTypeOfField($field) == 'id' && $field == 'realId') ||
295
                (
296
                    $field == 'id' &&
297
                    !$internal && $meta->isEmbeddedDocument && !in_array('id', $requiredFields) && $isEmptyExtref
298
                )
299
            ) {
300
                continue;
301
            }
302
303
            $property = new Schema();
304
            $property->setTitle($model->getTitleOfField($field));
305
            $property->setDescription($model->getDescriptionOfField($field));
306
            $property->setType($meta->getTypeOfField($field));
307
            $property->setGroups($model->getGroupsOfField($field));
308
            $property->setReadOnly($model->getReadOnlyOfField($field));
309
310
            // we only want to render if it's true
311
            if ($model->getRecordOriginExceptionOfField($field) === true) {
312
                $property->setRecordOriginException(true);
313
            }
314
315
            if ($meta->getTypeOfField($field) === 'many') {
316
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
317
318
                if ($model->hasDynamicKey($field)) {
319
                    $property->setType('object');
320
321
                    if ($online) {
322
                        // we generate a complete list of possible keys when we have access to mongodb
323
                        // this makes everything work with most json-schema v3 implementations (ie. schemaform.io)
324
                        $dynamicKeySpec = $model->getDynamicKeySpec($field);
325
326
                        $documentId = $dynamicKeySpec->{'document-id'};
327
                        $dynamicRepository = $this->repositoryFactory->get($documentId);
328
329
                        // put this in invalidate map so when know we have to invalidate when this document is used
330
                        $invalidateCacheMap[$dynamicRepository->getDocumentName()][] = $cacheKey;
0 ignored issues
show
Bug introduced by
The method getDocumentName() does not seem to exist on object<Doctrine\Common\Persistence\ObjectManager>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
331
332
                        $repositoryMethod = $dynamicKeySpec->{'repository-method'};
333
                        $records = $dynamicRepository->$repositoryMethod();
334
335
                        $dynamicProperties = array_map(
336
                            function ($record) {
337
                                return $record->getId();
338
                            },
339
                            $records
340
                        );
341
                        foreach ($dynamicProperties as $propertyName) {
342
                            $property->addProperty(
343
                                $propertyName,
344
                                $this->getModelSchema($field, $propertyModel, $online, $internal)
345
                            );
346
                        }
347
                    } else {
348
                        // swagger case
349
                        $property->setAdditionalProperties(
350
                            new SchemaAdditionalProperties(
351
                                $this->getModelSchema($field, $propertyModel, $online, $internal)
352
                            )
353
                        );
354
                    }
355
                } else {
356
                    $property->setItems($this->getModelSchema($field, $propertyModel, $online, $internal));
357
                    $property->setType('array');
358
                }
359
            } elseif ($meta->getTypeOfField($field) === 'one') {
360
                $propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field));
361
                $property = $this->getModelSchema($field, $propertyModel, $online, $internal);
362
363
                if ($property->getSearchable()) {
364
                    foreach ($property->getSearchable() as $searchableSubField) {
0 ignored issues
show
Bug introduced by
The method getSearchable does only exist in Graviton\SchemaBundle\Document\Schema, but not in stdClass.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
365
                        $subSearchableFields[] = $field . '.' . $searchableSubField;
366
                    }
367
                }
368
            } elseif (in_array($field, $translatableFields, true)) {
369
                $property = $this->makeTranslatable($property, $languages);
370
            } elseif (in_array($field.'[]', $translatableFields, true)) {
371
                $property = $this->makeArrayTranslatable($property, $languages);
372
            } elseif ($meta->getTypeOfField($field) === 'extref') {
373
                $urls = array();
374
                $refCollections = $model->getRefCollectionOfField($field);
375
                foreach ($refCollections as $collection) {
376
                    if (isset($this->extrefServiceMapping[$collection])) {
377
                        $urls[] = $this->router->generate(
378
                            $this->extrefServiceMapping[$collection].'.all',
379
                            [],
380
                            UrlGeneratorInterface::ABSOLUTE_URL
381
                        );
382
                    } elseif ($collection === '*') {
383
                        $urls[] = '*';
384
                    }
385
                }
386
                $property->setRefCollection($urls);
387 View Code Duplication
            } elseif ($meta->getTypeOfField($field) === 'collection') {
388
                $itemSchema = new Schema();
389
                $property->setType('array');
390
                $itemSchema->setType($this->getCollectionItemType($meta->name, $field));
391
392
                $property->setItems($itemSchema);
393
                $property->setFormat(null);
394
            } elseif ($meta->getTypeOfField($field) === 'datearray') {
395
                $itemSchema = new Schema();
396
                $property->setType('array');
397
                $itemSchema->setType('string');
398
                $itemSchema->setFormat('date-time');
399
400
                $property->setItems($itemSchema);
401
                $property->setFormat(null);
402 View Code Duplication
            } elseif ($meta->getTypeOfField($field) === 'hasharray') {
403
                $itemSchema = new Schema();
404
                $itemSchema->setType('object');
405
406
                $property->setType('array');
407
                $property->setItems($itemSchema);
408
                $property->setFormat(null);
409
            }
410
411
            if (in_array($meta->getTypeOfField($field), $property->getMinLengthTypes())) {
0 ignored issues
show
Bug introduced by
The method getMinLengthTypes does only exist in Graviton\SchemaBundle\Document\Schema, but not in stdClass.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
412
                // make sure a required field cannot be blank
413
                if (in_array($documentFieldNames[$field], $requiredFields)) {
414
                    $property->setMinLength(1);
0 ignored issues
show
Bug introduced by
The method setMinLength does only exist in Graviton\SchemaBundle\Document\Schema, but not in stdClass.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
415
                } else {
416
                    // in the other case, make sure also null can be sent..
417
                    $currentType = $property->getType();
0 ignored issues
show
Bug introduced by
The method getType does only exist in Graviton\SchemaBundle\Document\Schema, but not in stdClass.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
418
                    if ($currentType instanceof SchemaType) {
419
                        $property->setType(array_merge($currentType->getTypes(), ['null']));
0 ignored issues
show
Bug introduced by
The method setType does only exist in Graviton\SchemaBundle\Document\Schema, but not in stdClass.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
420
                    } else {
421
                        $property->setType('null');
422
                    }
423
                }
424
            }
425
426
            $property = $this->constraintBuilder->addConstraints($field, $property, $model);
0 ignored issues
show
Bug introduced by
It seems like $property can also be of type object<stdClass>; however, Graviton\SchemaBundle\Co...ilder::addConstraints() does only seem to accept object<Graviton\SchemaBundle\Document\Schema>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
427
428
            $schema->addProperty($documentFieldNames[$field], $property);
429
        }
430
431
        /**
432
         * if we generate schema for internal use; don't have id in required array as
433
         * it's 'requiredness' depends on the method used (POST/PUT/PATCH) and is checked in checks
434
         * before validation.
435
         */
436
        $idPosition = array_search('id', $requiredFields);
437
        if ($internal === true && $idPosition !== false && !$meta->isEmbeddedDocument) {
438
            unset($requiredFields[$idPosition]);
439
        }
440
441
        $schema->setRequired($requiredFields);
442
443
        // set additionalProperties to false (as this is our default policy) if not already set
444
        if (is_null($schema->getAdditionalProperties()) && $online) {
445
            $schema->setAdditionalProperties(new SchemaAdditionalProperties(false));
446
        }
447
448
        $searchableFields = array_merge($subSearchableFields, $model->getSearchableFields());
449
        $schema->setSearchable($searchableFields);
450
451
        if ($serialized === true) {
452
            $schema = json_decode($this->serializer->serialize($schema, 'json'));
453
        }
454
455
        $this->cache->save($cacheKey, $schema);
456
        $this->cache->save($this->cacheInvalidationMapKey, $invalidateCacheMap);
457
458
        return $schema;
459
    }
460
461
    /**
462
     * turn a property into a translatable property
463
     *
464
     * @param Schema   $property  simple string property
465
     * @param string[] $languages available languages
466
     *
467
     * @return Schema
468
     */
469
    public function makeTranslatable(Schema $property, $languages)
470
    {
471
        $property->setType('object');
472
        $property->setTranslatable(true);
473
474
        array_walk(
475
            $languages,
476
            function ($language) use ($property) {
477
                $schema = new Schema;
478
                $schema->setType('string');
479
                $schema->setTitle('Translated String');
480
                $schema->setDescription('String in ' . $language . ' locale.');
481
                $property->addProperty($language, $schema);
482
            }
483
        );
484
        $property->setRequired(['en']);
485
        return $property;
486
    }
487
488
    /**
489
     * turn a array property into a translatable property
490
     *
491
     * @param Schema   $property  simple string property
492
     * @param string[] $languages available languages
493
     *
494
     * @return Schema
495
     */
496
    public function makeArrayTranslatable(Schema $property, $languages)
497
    {
498
        $property->setType('array');
499
        $property->setItems($this->makeTranslatable(new Schema(), $languages));
500
        return $property;
501
    }
502
503
    /**
504
     * get canonical route to a schema based on a route
505
     *
506
     * @param string $routeName route name
507
     *
508
     * @return string schema route name
509
     */
510
    public static function getSchemaRouteName($routeName)
511
    {
512
        $routeParts = explode('.', $routeName);
513
514
        $routeType = array_pop($routeParts);
515
        // check if we need to create an item or collection schema
516
        $realRouteType = 'canonicalSchema';
517
        if ($routeType != 'options' && $routeType != 'all') {
518
            $realRouteType = 'canonicalIdSchema';
519
        }
520
521
        return implode('.', array_merge($routeParts, array($realRouteType)));
522
    }
523
524
    /**
525
     * Get item type of collection field
526
     *
527
     * @param string $className Class name
528
     * @param string $fieldName Field name
529
     * @return string|null
530
     */
531
    private function getCollectionItemType($className, $fieldName)
532
    {
533
        $serializerMetadata = $this->serializerMetadataFactory->getMetadataForClass($className);
534
        if ($serializerMetadata === null) {
535
            return null;
536
        }
537
        if (!isset($serializerMetadata->propertyMetadata[$fieldName])) {
538
            return null;
539
        }
540
541
        $type = $serializerMetadata->propertyMetadata[$fieldName]->type;
542
        return isset($type['name'], $type['params'][0]['name']) && $type['name'] === 'array' ?
543
            $type['params'][0]['name'] :
544
            null;
545
    }
546
}
547