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
|
|
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* Utils for generating schemas. |
20
|
|
|
* |
21
|
|
|
* @author List of contributors <https://github.com/libgraviton/graviton/graphs/contributors> |
22
|
|
|
* @license http://opensource.org/licenses/gpl-license.php GNU Public License |
23
|
|
|
* @link http://swisscom.ch |
24
|
|
|
*/ |
25
|
|
|
class SchemaUtils |
26
|
|
|
{ |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* language repository |
30
|
|
|
* |
31
|
|
|
* @var LanguageRepository repository |
32
|
|
|
*/ |
33
|
|
|
private $languageRepository; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* router |
37
|
|
|
* |
38
|
|
|
* @var RouterInterface router |
39
|
|
|
*/ |
40
|
|
|
private $router; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* mapping service names => route names |
44
|
|
|
* |
45
|
|
|
* @var array service mapping |
46
|
|
|
*/ |
47
|
|
|
private $extrefServiceMapping; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* event map |
51
|
|
|
* |
52
|
|
|
* @var array event map |
53
|
|
|
*/ |
54
|
|
|
private $eventMap; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var array [document class => [field name -> exposed name]] |
58
|
|
|
*/ |
59
|
|
|
private $documentFieldNames; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @var string |
63
|
|
|
*/ |
64
|
|
|
private $defaultLocale; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @var RepositoryFactory |
68
|
|
|
*/ |
69
|
|
|
private $repositoryFactory; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @var SerializerMetadataFactoryInterface |
73
|
|
|
*/ |
74
|
|
|
private $serializerMetadataFactory; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Constructor |
78
|
|
|
* |
79
|
|
|
* @param RepositoryFactory $repositoryFactory Create repos from model class names |
80
|
|
|
* @param SerializerMetadataFactoryInterface $serializerMetadataFactory Serializer metadata factory |
81
|
|
|
* @param LanguageRepository $languageRepository repository |
82
|
|
|
* @param RouterInterface $router router |
83
|
|
|
* @param array $extrefServiceMapping Extref service mapping |
84
|
|
|
* @param array $eventMap eventmap |
85
|
|
|
* @param array $documentFieldNames Document field names |
86
|
|
|
* @param string $defaultLocale Default Language |
87
|
|
|
*/ |
88
|
2 |
|
public function __construct( |
89
|
|
|
RepositoryFactory $repositoryFactory, |
90
|
|
|
SerializerMetadataFactoryInterface $serializerMetadataFactory, |
91
|
|
|
LanguageRepository $languageRepository, |
92
|
|
|
RouterInterface $router, |
93
|
|
|
array $extrefServiceMapping, |
94
|
|
|
array $eventMap, |
95
|
|
|
array $documentFieldNames, |
96
|
|
|
$defaultLocale |
97
|
|
|
) { |
98
|
2 |
|
$this->repositoryFactory = $repositoryFactory; |
99
|
2 |
|
$this->serializerMetadataFactory = $serializerMetadataFactory; |
100
|
2 |
|
$this->languageRepository = $languageRepository; |
101
|
2 |
|
$this->router = $router; |
102
|
2 |
|
$this->extrefServiceMapping = $extrefServiceMapping; |
103
|
2 |
|
$this->eventMap = $eventMap; |
104
|
2 |
|
$this->documentFieldNames = $documentFieldNames; |
105
|
2 |
|
$this->defaultLocale = $defaultLocale; |
106
|
2 |
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* get schema for an array of models |
110
|
|
|
* |
111
|
|
|
* @param string $modelName name of model |
112
|
|
|
* @param DocumentModel $model model |
113
|
|
|
* |
114
|
|
|
* @return Schema |
115
|
|
|
*/ |
116
|
|
|
public function getCollectionSchema($modelName, DocumentModel $model) |
117
|
|
|
{ |
118
|
|
|
$collectionSchema = new Schema; |
119
|
|
|
$collectionSchema->setTitle(sprintf('Array of %s objects', $modelName)); |
120
|
|
|
$collectionSchema->setType('array'); |
121
|
|
|
|
122
|
|
|
$collectionSchema->setItems($this->getModelSchema($modelName, $model)); |
123
|
|
|
|
124
|
|
|
return $collectionSchema; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* return the schema for a given route |
129
|
|
|
* |
130
|
|
|
* @param string $modelName name of mode to generate schema for |
131
|
|
|
* @param DocumentModel $model model to generate schema for |
132
|
|
|
* @param boolean $online if we are online and have access to mongodb during this build |
133
|
|
|
* |
134
|
|
|
* @return Schema |
135
|
|
|
*/ |
136
|
|
|
public function getModelSchema($modelName, DocumentModel $model, $online = true) |
137
|
|
|
{ |
138
|
|
|
// build up schema data |
139
|
|
|
$schema = new Schema; |
140
|
|
|
|
141
|
|
|
if (!empty($model->getTitle())) { |
142
|
|
|
$schema->setTitle($model->getTitle()); |
143
|
|
|
} else { |
144
|
|
|
$schema->setTitle(ucfirst($modelName)); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
$schema->setDescription($model->getDescription()); |
148
|
|
|
$schema->setType('object'); |
149
|
|
|
|
150
|
|
|
// grab schema info from model |
151
|
|
|
$repo = $model->getRepository(); |
152
|
|
|
$meta = $repo->getClassMetadata(); |
153
|
|
|
|
154
|
|
|
// Init sub searchable fields |
155
|
|
|
$subSearchableFields = array(); |
156
|
|
|
|
157
|
|
|
// look for translatables in document class |
158
|
|
|
$documentReflection = new \ReflectionClass($repo->getClassName()); |
159
|
|
|
if ($documentReflection->implementsInterface('Graviton\I18nBundle\Document\TranslatableDocumentInterface')) { |
160
|
|
|
/** @var TranslatableDocumentInterface $documentInstance */ |
161
|
|
|
$documentInstance = $documentReflection->newInstanceWithoutConstructor(); |
162
|
|
|
$translatableFields = array_merge( |
163
|
|
|
$documentInstance->getTranslatableFields(), |
164
|
|
|
$documentInstance->getPreTranslatedFields() |
165
|
|
|
); |
166
|
|
|
} else { |
167
|
|
|
$translatableFields = []; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
// exposed fields |
171
|
|
|
$documentFieldNames = isset($this->documentFieldNames[$repo->getClassName()]) ? |
172
|
|
|
$this->documentFieldNames[$repo->getClassName()] : |
173
|
|
|
[]; |
174
|
|
|
|
175
|
|
|
if ($online) { |
176
|
|
|
$languages = array_map( |
177
|
|
|
function (Language $language) { |
178
|
|
|
return $language->getId(); |
179
|
|
|
}, |
180
|
|
|
$this->languageRepository->findAll() |
181
|
|
|
); |
182
|
|
|
} else { |
183
|
|
|
$languages = [ |
184
|
|
|
$this->defaultLocale |
185
|
|
|
]; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
// exposed events.. |
189
|
|
|
$classShortName = $documentReflection->getShortName(); |
190
|
|
|
if (isset($this->eventMap[$classShortName])) { |
191
|
|
|
$schema->setEventNames(array_unique($this->eventMap[$classShortName]['events'])); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
foreach ($meta->getFieldNames() as $field) { |
195
|
|
|
// don't describe hidden fields |
196
|
|
|
if (!isset($documentFieldNames[$field])) { |
197
|
|
|
continue; |
198
|
|
|
} |
199
|
|
|
// hide realId field (I was aiming at a cleaner solution than the macig realId string initially) |
200
|
|
|
if ($meta->getTypeOfField($field) == 'id' && $field == 'realId') { |
201
|
|
|
continue; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
$property = new Schema(); |
205
|
|
|
$property->setTitle($model->getTitleOfField($field)); |
206
|
|
|
$property->setDescription($model->getDescriptionOfField($field)); |
207
|
|
|
|
208
|
|
|
$property->setType($meta->getTypeOfField($field)); |
209
|
|
|
$property->setReadOnly($model->getReadOnlyOfField($field)); |
210
|
|
|
|
211
|
|
|
if ($meta->getTypeOfField($field) === 'many') { |
212
|
|
|
$propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field)); |
213
|
|
|
|
214
|
|
|
if ($model->hasDynamicKey($field)) { |
215
|
|
|
$property->setType('object'); |
216
|
|
|
|
217
|
|
|
if ($online) { |
218
|
|
|
// we generate a complete list of possible keys when we have access to mongodb |
219
|
|
|
// this makes everything work with most json-schema v3 implementations (ie. schemaform.io) |
220
|
|
|
$dynamicKeySpec = $model->getDynamicKeySpec($field); |
221
|
|
|
|
222
|
|
|
$documentId = $dynamicKeySpec->{'document-id'}; |
223
|
|
|
$dynamicRepository = $this->repositoryFactory->get($documentId); |
224
|
|
|
|
225
|
|
|
$repositoryMethod = $dynamicKeySpec->{'repository-method'}; |
226
|
|
|
$records = $dynamicRepository->$repositoryMethod(); |
227
|
|
|
|
228
|
|
|
$dynamicProperties = array_map( |
229
|
|
|
function ($record) { |
230
|
|
|
return $record->getId(); |
231
|
|
|
}, |
232
|
|
|
$records |
233
|
|
|
); |
234
|
|
|
foreach ($dynamicProperties as $propertyName) { |
235
|
|
|
$property->addProperty( |
236
|
|
|
$propertyName, |
237
|
|
|
$this->getModelSchema($field, $propertyModel, $online) |
238
|
|
|
); |
239
|
|
|
} |
240
|
|
|
} else { |
241
|
|
|
// in the swagger case we can use additionPorerties which where introduced by json-schema v4 |
242
|
|
|
$property->setAdditionalProperties($this->getModelSchema($field, $propertyModel, $online)); |
243
|
|
|
} |
244
|
|
|
} else { |
245
|
|
|
$property->setItems($this->getModelSchema($field, $propertyModel, $online)); |
246
|
|
|
$property->setType('array'); |
247
|
|
|
} |
248
|
|
|
} elseif ($meta->getTypeOfField($field) === 'one') { |
249
|
|
|
$propertyModel = $model->manyPropertyModelForTarget($meta->getAssociationTargetClass($field)); |
250
|
|
|
$property = $this->getModelSchema($field, $propertyModel, $online); |
251
|
|
|
|
252
|
|
|
if ($property->getSearchable()) { |
253
|
|
|
foreach ($property->getSearchable() as $searchableSubField) { |
254
|
|
|
$subSearchableFields[] = $field . '.' . $searchableSubField; |
255
|
|
|
} |
256
|
|
|
} |
257
|
|
|
} elseif (in_array($field, $translatableFields, true)) { |
258
|
|
|
$property = $this->makeTranslatable($property, $languages); |
259
|
|
|
} elseif (in_array($field.'[]', $translatableFields, true)) { |
260
|
|
|
$property = $this->makeArrayTranslatable($property, $languages); |
261
|
|
|
} elseif ($meta->getTypeOfField($field) === 'extref') { |
262
|
|
|
$urls = array(); |
263
|
|
|
$refCollections = $model->getRefCollectionOfField($field); |
264
|
|
|
foreach ($refCollections as $collection) { |
265
|
|
|
if (isset($this->extrefServiceMapping[$collection])) { |
266
|
|
|
$urls[] = $this->router->generate( |
267
|
|
|
$this->extrefServiceMapping[$collection].'.all', |
268
|
|
|
[], |
269
|
|
|
UrlGeneratorInterface::ABSOLUTE_URL |
270
|
|
|
); |
271
|
|
|
} elseif ($collection === '*') { |
272
|
|
|
$urls[] = '*'; |
273
|
|
|
} |
274
|
|
|
} |
275
|
|
|
$property->setRefCollection($urls); |
276
|
|
View Code Duplication |
} elseif ($meta->getTypeOfField($field) === 'collection') { |
|
|
|
|
277
|
|
|
$itemSchema = new Schema(); |
278
|
|
|
$property->setType('array'); |
279
|
|
|
$itemSchema->setType($this->getCollectionItemType($meta->name, $field)); |
280
|
|
|
|
281
|
|
|
$property->setItems($itemSchema); |
282
|
|
|
$property->setFormat(null); |
283
|
|
|
} elseif ($meta->getTypeOfField($field) === 'datearray') { |
284
|
|
|
$itemSchema = new Schema(); |
285
|
|
|
$property->setType('array'); |
286
|
|
|
$itemSchema->setType('string'); |
287
|
|
|
$itemSchema->setFormat('date-time'); |
288
|
|
|
|
289
|
|
|
$property->setItems($itemSchema); |
290
|
|
|
$property->setFormat(null); |
291
|
|
View Code Duplication |
} elseif ($meta->getTypeOfField($field) === 'hasharray') { |
|
|
|
|
292
|
|
|
$itemSchema = new Schema(); |
293
|
|
|
$itemSchema->setType('object'); |
294
|
|
|
|
295
|
|
|
$property->setType('array'); |
296
|
|
|
$property->setItems($itemSchema); |
297
|
|
|
$property->setFormat(null); |
298
|
|
|
} |
299
|
|
|
$schema->addProperty($documentFieldNames[$field], $property); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
if ($meta->isEmbeddedDocument && !in_array('id', $model->getRequiredFields())) { |
303
|
|
|
$schema->removeProperty('id'); |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
$requiredFields = []; |
307
|
|
|
$modelRequiredFields = $model->getRequiredFields(); |
308
|
|
|
if (is_array($modelRequiredFields)) { |
309
|
|
|
foreach ($modelRequiredFields as $field) { |
310
|
|
|
// don't describe hidden fields |
311
|
|
|
if (!isset($documentFieldNames[$field])) { |
312
|
|
|
continue; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
$requiredFields[] = $documentFieldNames[$field]; |
316
|
|
|
} |
317
|
|
|
} |
318
|
|
|
$schema->setRequired($requiredFields); |
319
|
|
|
|
320
|
|
|
$searchableFields = array_merge($subSearchableFields, $model->getSearchableFields()); |
321
|
|
|
|
322
|
|
|
$schema->setSearchable($searchableFields); |
323
|
|
|
|
324
|
|
|
return $schema; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* turn a property into a translatable property |
329
|
|
|
* |
330
|
|
|
* @param Schema $property simple string property |
331
|
|
|
* @param string[] $languages available languages |
332
|
|
|
* |
333
|
|
|
* @return Schema |
334
|
|
|
*/ |
335
|
|
|
public function makeTranslatable(Schema $property, $languages) |
336
|
|
|
{ |
337
|
|
|
$property->setType('object'); |
338
|
|
|
$property->setTranslatable(true); |
339
|
|
|
|
340
|
|
|
array_walk( |
341
|
|
|
$languages, |
342
|
|
|
function ($language) use ($property) { |
343
|
|
|
$schema = new Schema; |
344
|
|
|
$schema->setType('string'); |
345
|
|
|
$schema->setTitle('Translated String'); |
346
|
|
|
$schema->setDescription('String in ' . $language . ' locale.'); |
347
|
|
|
$property->addProperty($language, $schema); |
348
|
|
|
} |
349
|
|
|
); |
350
|
|
|
$property->setRequired(['en']); |
351
|
|
|
return $property; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* turn a array property into a translatable property |
356
|
|
|
* |
357
|
|
|
* @param Schema $property simple string property |
358
|
|
|
* @param string[] $languages available languages |
359
|
|
|
* |
360
|
|
|
* @return Schema |
361
|
|
|
*/ |
362
|
|
|
public function makeArrayTranslatable(Schema $property, $languages) |
363
|
|
|
{ |
364
|
|
|
$property->setType('array'); |
365
|
|
|
$property->setItems($this->makeTranslatable(new Schema(), $languages)); |
366
|
|
|
return $property; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* get canonical route to a schema based on a route |
371
|
|
|
* |
372
|
|
|
* @param string $routeName route name |
373
|
|
|
* |
374
|
|
|
* @return string schema route name |
375
|
|
|
*/ |
376
|
|
|
public static function getSchemaRouteName($routeName) |
377
|
|
|
{ |
378
|
|
|
$routeParts = explode('.', $routeName); |
379
|
|
|
|
380
|
|
|
$routeType = array_pop($routeParts); |
381
|
|
|
// check if we need to create an item or collection schema |
382
|
|
|
$realRouteType = 'canonicalSchema'; |
383
|
|
|
if ($routeType != 'options' && $routeType != 'all') { |
384
|
|
|
$realRouteType = 'canonicalIdSchema'; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
return implode('.', array_merge($routeParts, array($realRouteType))); |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* Get item type of collection field |
392
|
|
|
* |
393
|
|
|
* @param string $className Class name |
394
|
|
|
* @param string $fieldName Field name |
395
|
|
|
* @return string|null |
396
|
|
|
*/ |
397
|
|
|
private function getCollectionItemType($className, $fieldName) |
398
|
|
|
{ |
399
|
|
|
$serializerMetadata = $this->serializerMetadataFactory->getMetadataForClass($className); |
400
|
|
|
if ($serializerMetadata === null) { |
401
|
|
|
return null; |
402
|
|
|
} |
403
|
|
|
if (!isset($serializerMetadata->propertyMetadata[$fieldName])) { |
404
|
|
|
return null; |
405
|
|
|
} |
406
|
|
|
|
407
|
|
|
$type = $serializerMetadata->propertyMetadata[$fieldName]->type; |
408
|
|
|
return isset($type['name'], $type['params'][0]['name']) && $type['name'] === 'array' ? |
409
|
|
|
$type['params'][0]['name'] : |
410
|
|
|
null; |
411
|
|
|
} |
412
|
|
|
} |
413
|
|
|
|
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.