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 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 |
||
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) |
||
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) |
||
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) |
||
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) |
||
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) |
||
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.