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 DocumentSchema 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 DocumentSchema, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 22 | class DocumentSchema implements SchemaInterface |
||
| 23 | { |
||
| 24 | /** |
||
| 25 | * @var ReflectionEntity |
||
| 26 | */ |
||
| 27 | private $reflection; |
||
| 28 | |||
| 29 | /** |
||
| 30 | * @invisible |
||
| 31 | * |
||
| 32 | * @var MutatorsConfig |
||
| 33 | */ |
||
| 34 | private $mutatorsConfig; |
||
| 35 | |||
| 36 | /** |
||
| 37 | * @param ReflectionEntity $reflection |
||
| 38 | * @param MutatorsConfig $mutators |
||
| 39 | */ |
||
| 40 | public function __construct(ReflectionEntity $reflection, MutatorsConfig $mutators) |
||
| 41 | { |
||
| 42 | $this->reflection = $reflection; |
||
| 43 | $this->mutatorsConfig = $mutators; |
||
| 44 | } |
||
| 45 | |||
| 46 | /** |
||
| 47 | * @return string |
||
| 48 | */ |
||
| 49 | public function getClass(): string |
||
| 50 | { |
||
| 51 | return $this->reflection->getName(); |
||
| 52 | } |
||
| 53 | |||
| 54 | /** |
||
| 55 | * @return ReflectionEntity |
||
| 56 | */ |
||
| 57 | public function getReflection(): ReflectionEntity |
||
| 58 | { |
||
| 59 | return $this->reflection; |
||
| 60 | } |
||
| 61 | |||
| 62 | /** |
||
| 63 | * @return string |
||
| 64 | */ |
||
| 65 | public function getInstantiator(): string |
||
| 66 | { |
||
| 67 | return $this->reflection->getProperty('instantiator') ?? DocumentInstantiator::class; |
||
| 68 | } |
||
| 69 | |||
| 70 | /** |
||
| 71 | * {@inheritdoc} |
||
| 72 | */ |
||
| 73 | public function isEmbedded(): bool |
||
| 74 | { |
||
| 75 | return !$this->reflection->isSubclassOf(Document::class) |
||
| 76 | && $this->reflection->isSubclassOf(DocumentEntity::class); |
||
| 77 | } |
||
| 78 | |||
| 79 | /** |
||
| 80 | * {@inheritdoc} |
||
| 81 | */ |
||
| 82 | public function getDatabase() |
||
| 83 | { |
||
| 84 | if ($this->isEmbedded()) { |
||
| 85 | throw new SchemaException( |
||
| 86 | "Unable to get database name for embedded model {$this->reflection}" |
||
| 87 | ); |
||
| 88 | } |
||
| 89 | |||
| 90 | $database = $this->reflection->getProperty('database'); |
||
| 91 | if (empty($database)) { |
||
| 92 | //Empty database to be used |
||
| 93 | return null; |
||
| 94 | } |
||
| 95 | |||
| 96 | return $database; |
||
| 97 | } |
||
| 98 | |||
| 99 | /** |
||
| 100 | * {@inheritdoc} |
||
| 101 | */ |
||
| 102 | public function getCollection(): string |
||
| 103 | { |
||
| 104 | if ($this->isEmbedded()) { |
||
| 105 | throw new SchemaException( |
||
| 106 | "Unable to get collection name for embedded model {$this->reflection}" |
||
| 107 | ); |
||
| 108 | } |
||
| 109 | |||
| 110 | $collection = $this->reflection->getProperty('collection'); |
||
| 111 | if (empty($collection)) { |
||
| 112 | //Generate collection using short class name |
||
| 113 | $collection = Inflector::camelize($this->reflection->getShortName()); |
||
| 114 | $collection = Inflector::pluralize($collection); |
||
| 115 | } |
||
| 116 | |||
| 117 | return $collection; |
||
| 118 | } |
||
| 119 | |||
| 120 | /** |
||
| 121 | * Get every embedded entity field (excluding declarations of aggregations). |
||
| 122 | * |
||
| 123 | * @return array |
||
| 124 | */ |
||
| 125 | View Code Duplication | public function getFields(): array |
|
|
|
|||
| 126 | { |
||
| 127 | $fields = $this->reflection->getFields(); |
||
| 128 | |||
| 129 | foreach ($fields as $field => $type) { |
||
| 130 | if ($this->isAggregation($type)) { |
||
| 131 | unset($fields[$field]); |
||
| 132 | } |
||
| 133 | } |
||
| 134 | |||
| 135 | return $fields; |
||
| 136 | } |
||
| 137 | |||
| 138 | /** |
||
| 139 | * Default defined values. |
||
| 140 | * |
||
| 141 | * @return array |
||
| 142 | */ |
||
| 143 | public function getDefaults(): array |
||
| 144 | { |
||
| 145 | return $this->reflection->getProperty('defaults') ?? []; |
||
| 146 | } |
||
| 147 | |||
| 148 | /** |
||
| 149 | * {@inheritdoc} |
||
| 150 | */ |
||
| 151 | public function getIndexes(): array |
||
| 152 | { |
||
| 153 | if ($this->isEmbedded()) { |
||
| 154 | throw new SchemaException( |
||
| 155 | "Unable to get indexes for embedded model {$this->reflection}" |
||
| 156 | ); |
||
| 157 | } |
||
| 158 | |||
| 159 | $indexes = $this->reflection->getProperty('indexes', true); |
||
| 160 | if (empty($indexes) || !is_array($indexes)) { |
||
| 161 | return []; |
||
| 162 | } |
||
| 163 | |||
| 164 | $result = []; |
||
| 165 | foreach ($indexes as $index) { |
||
| 166 | $options = []; |
||
| 167 | if (isset($index['@options'])) { |
||
| 168 | $options = $index['@options']; |
||
| 169 | unset($index['@options']); |
||
| 170 | } |
||
| 171 | |||
| 172 | $result[] = new IndexDefinition($index, $options); |
||
| 173 | } |
||
| 174 | |||
| 175 | return array_unique($result); |
||
| 176 | } |
||
| 177 | |||
| 178 | /** |
||
| 179 | * @return AggregationDefinition[] |
||
| 180 | */ |
||
| 181 | public function getAggregations(): array |
||
| 182 | { |
||
| 183 | $result = []; |
||
| 184 | foreach ($this->reflection->getFields() as $field => $type) { |
||
| 185 | if ($this->isAggregation($type)) { |
||
| 186 | $aggregationType = isset($type[Document::ONE]) ? Document::ONE : Document::MANY; |
||
| 187 | |||
| 188 | $result[$field] = new AggregationDefinition( |
||
| 189 | $aggregationType, //Aggregation type |
||
| 190 | $type[$aggregationType], //Class name |
||
| 191 | array_pop($type) //Query template |
||
| 192 | ); |
||
| 193 | } |
||
| 194 | } |
||
| 195 | |||
| 196 | return $result; |
||
| 197 | } |
||
| 198 | |||
| 199 | /** |
||
| 200 | * Find all composition definitions, attention method require builder instance in order to |
||
| 201 | * properly check that embedded class exists. |
||
| 202 | * |
||
| 203 | * @param SchemaBuilder $builder |
||
| 204 | * |
||
| 205 | * @return CompositionDefinition[] |
||
| 206 | */ |
||
| 207 | public function getCompositions(SchemaBuilder $builder): array |
||
| 208 | { |
||
| 209 | $result = []; |
||
| 210 | foreach ($this->reflection->getFields() as $field => $type) { |
||
| 211 | if (is_string($type) && $builder->hasSchema($type)) { |
||
| 212 | $result[$field] = new CompositionDefinition(DocumentEntity::ONE, $type); |
||
| 213 | } |
||
| 214 | |||
| 215 | if (is_array($type) && isset($type[0]) && $builder->hasSchema($type[0])) { |
||
| 216 | $result[$field] = new CompositionDefinition(DocumentEntity::MANY, $type[0]); |
||
| 217 | } |
||
| 218 | } |
||
| 219 | |||
| 220 | return $result; |
||
| 221 | } |
||
| 222 | |||
| 223 | /** |
||
| 224 | * {@inheritdoc} |
||
| 225 | */ |
||
| 226 | public function resolvePrimary(SchemaBuilder $builder): string |
||
| 227 | { |
||
| 228 | //Let's define a way how to separate one model from another based on given fields |
||
| 229 | $helper = new InheritanceHelper($this, $builder->getSchemas()); |
||
| 230 | |||
| 231 | return $helper->findPrimary(); |
||
| 232 | } |
||
| 233 | |||
| 234 | /** |
||
| 235 | * {@inheritdoc} |
||
| 236 | */ |
||
| 237 | public function packSchema(SchemaBuilder $builder): array |
||
| 238 | { |
||
| 239 | return [ |
||
| 240 | //Instantion options and behaviour (if any) |
||
| 241 | DocumentEntity::SH_INSTANTIATION => $this->instantiationOptions($builder), |
||
| 242 | |||
| 243 | //Default entity state (builder is needed to resolve recursive defaults) |
||
| 244 | DocumentEntity::SH_DEFAULTS => $this->packDefaults($builder), |
||
| 245 | |||
| 246 | //Entity behaviour |
||
| 247 | DocumentEntity::SH_HIDDEN => $this->reflection->getHidden(), |
||
| 248 | DocumentEntity::SH_SECURED => $this->reflection->getSecured(), |
||
| 249 | DocumentEntity::SH_FILLABLE => $this->reflection->getFillable(), |
||
| 250 | |||
| 251 | //Mutators can be altered based on ODM\SchemasConfig |
||
| 252 | DocumentEntity::SH_MUTATORS => $this->resolveMutators(), |
||
| 253 | |||
| 254 | //Document behaviours (we can mix them with accessors due potential inheritance) |
||
| 255 | DocumentEntity::SH_COMPOSITIONS => $this->packCompositions($builder), |
||
| 256 | DocumentEntity::SH_AGGREGATIONS => $this->packAggregations($builder), |
||
| 257 | ]; |
||
| 258 | } |
||
| 259 | |||
| 260 | /** |
||
| 261 | * Define instantiator specific options (usually needed to resolve class inheritance). Might |
||
| 262 | * return null if associated instantiator is unknown to DocumentSchema. |
||
| 263 | * |
||
| 264 | * @param SchemaBuilder $builder |
||
| 265 | * |
||
| 266 | * @return mixed |
||
| 267 | */ |
||
| 268 | protected function instantiationOptions(SchemaBuilder $builder) |
||
| 269 | { |
||
| 270 | if ($this->getInstantiator() != DocumentInstantiator::class) { |
||
| 271 | //Unable to define options for non default inheritance based instantiator |
||
| 272 | return null; |
||
| 273 | } |
||
| 274 | |||
| 275 | //Let's define a way how to separate one model from another based on given fields |
||
| 276 | $helper = new InheritanceHelper($this, $builder->getSchemas()); |
||
| 277 | |||
| 278 | return $helper->makeDefinition(); |
||
| 279 | } |
||
| 280 | |||
| 281 | /** |
||
| 282 | * Entity default values. |
||
| 283 | * |
||
| 284 | * @param SchemaBuilder $builder |
||
| 285 | * @param array $overwriteDefaults Set of default values to replace user defined values. |
||
| 286 | * |
||
| 287 | * @return array |
||
| 288 | * |
||
| 289 | * @throws SchemaException |
||
| 290 | */ |
||
| 291 | protected function packDefaults(SchemaBuilder $builder, array $overwriteDefaults = []): array |
||
| 292 | { |
||
| 293 | //Defined compositions |
||
| 294 | $compositions = $this->getCompositions($builder); |
||
| 295 | |||
| 296 | //User defined default values |
||
| 297 | $userDefined = $overwriteDefaults + $this->getDefaults(); |
||
| 298 | |||
| 299 | //We need mutators to normalize default values |
||
| 300 | $mutators = $this->resolveMutators(); |
||
| 301 | |||
| 302 | $defaults = []; |
||
| 303 | foreach ($this->getFields() as $field => $type) { |
||
| 304 | $default = is_array($type) ? [] : null; |
||
| 305 | |||
| 306 | if (array_key_exists($field, $userDefined)) { |
||
| 307 | //No merge to keep fields order intact |
||
| 308 | $default = $userDefined[$field]; |
||
| 309 | } |
||
| 310 | |||
| 311 | if (array_key_exists($field, $defaults)) { |
||
| 312 | //Default value declared in model schema |
||
| 313 | $default = $defaults[$field]; |
||
| 314 | } |
||
| 315 | |||
| 316 | //Let's process default value using associated setter |
||
| 317 | if (isset($mutators[DocumentEntity::MUTATOR_SETTER][$field])) { |
||
| 318 | try { |
||
| 319 | $setter = $mutators[DocumentEntity::MUTATOR_SETTER][$field]; |
||
| 320 | $default = call_user_func($setter, $default); |
||
| 321 | } catch (\Exception $exception) { |
||
| 322 | //Unable to generate default value, use null or empty array as fallback |
||
| 323 | } |
||
| 324 | } |
||
| 325 | |||
| 326 | if (isset($mutators[DocumentEntity::MUTATOR_ACCESSOR][$field])) { |
||
| 327 | $default = $this->accessorDefault( |
||
| 328 | $default, |
||
| 329 | $mutators[DocumentEntity::MUTATOR_ACCESSOR][$field] |
||
| 330 | ); |
||
| 331 | } |
||
| 332 | |||
| 333 | if (isset($compositions[$field])) { |
||
| 334 | if (is_null($default) && !array_key_exists($field, $userDefined)) { |
||
| 335 | //Let's force default value for composite fields |
||
| 336 | $default = []; |
||
| 337 | } |
||
| 338 | |||
| 339 | $default = $this->compositionDefault($default, $compositions[$field], $builder); |
||
| 340 | } |
||
| 341 | |||
| 342 | //Registering default values |
||
| 343 | $defaults[$field] = $default; |
||
| 344 | } |
||
| 345 | |||
| 346 | return $defaults; |
||
| 347 | } |
||
| 348 | |||
| 349 | /** |
||
| 350 | * Generate set of mutators associated with entity fields using user defined and automatic |
||
| 351 | * mutators. |
||
| 352 | * |
||
| 353 | * @see MutatorsConfig |
||
| 354 | * @return array |
||
| 355 | */ |
||
| 356 | protected function resolveMutators(): array |
||
| 357 | { |
||
| 358 | $mutators = $this->reflection->getMutators(); |
||
| 359 | |||
| 360 | //Trying to resolve mutators based on field type |
||
| 361 | foreach ($this->getFields() as $field => $type) { |
||
| 362 | //Resolved mutators |
||
| 363 | $resolved = []; |
||
| 364 | |||
| 365 | if ( |
||
| 366 | is_array($type) |
||
| 367 | && is_scalar($type[0]) |
||
| 368 | && $filter = $this->mutatorsConfig->getMutators('array::' . $type[0]) |
||
| 369 | ) { |
||
| 370 | //Mutator associated to array with specified type |
||
| 371 | $resolved += $filter; |
||
| 372 | } elseif (is_array($type) && $filter = $this->mutatorsConfig->getMutators('array')) { |
||
| 373 | //Default array mutator |
||
| 374 | $resolved += $filter; |
||
| 375 | } elseif (!is_array($type) && $filter = $this->mutatorsConfig->getMutators($type)) { |
||
| 376 | //Mutator associated with type directly |
||
| 377 | $resolved += $filter; |
||
| 378 | } |
||
| 379 | |||
| 380 | //Merging mutators and default mutators |
||
| 381 | foreach ($resolved as $mutator => $filter) { |
||
| 382 | if (!array_key_exists($field, $mutators[$mutator])) { |
||
| 383 | $mutators[$mutator][$field] = $filter; |
||
| 384 | } |
||
| 385 | } |
||
| 386 | } |
||
| 387 | |||
| 388 | return $mutators; |
||
| 389 | } |
||
| 390 | |||
| 391 | /** |
||
| 392 | * Pack compositions into simple array definition. |
||
| 393 | * |
||
| 394 | * @param SchemaBuilder $builder |
||
| 395 | * |
||
| 396 | * @return array |
||
| 397 | * |
||
| 398 | * @throws SchemaException |
||
| 399 | */ |
||
| 400 | public function packCompositions(SchemaBuilder $builder): array |
||
| 409 | |||
| 410 | /** |
||
| 411 | * Pack aggregations into simple array definition. |
||
| 412 | * |
||
| 413 | * @param SchemaBuilder $builder |
||
| 414 | * |
||
| 415 | * @return array |
||
| 416 | * |
||
| 417 | * @throws SchemaException |
||
| 418 | */ |
||
| 419 | protected function packAggregations(SchemaBuilder $builder): array |
||
| 440 | |||
| 441 | /** |
||
| 442 | * Check if field schema/type defines aggregation. |
||
| 443 | * |
||
| 444 | * @param mixed $type |
||
| 445 | * |
||
| 446 | * @return bool |
||
| 447 | */ |
||
| 448 | protected function isAggregation($type): bool |
||
| 458 | |||
| 459 | /** |
||
| 460 | * Pass value thought accessor to ensure it's default. |
||
| 461 | * |
||
| 462 | * @param mixed $default |
||
| 463 | * @param string $accessor |
||
| 464 | * |
||
| 465 | * @return mixed |
||
| 466 | * |
||
| 467 | * @throws AccessorException |
||
| 468 | */ |
||
| 469 | protected function accessorDefault($default, string $accessor) |
||
| 470 | { |
||
| 471 | /** |
||
| 472 | * @var AccessorInterface $instance |
||
| 473 | */ |
||
| 474 | $instance = new $accessor($default, [/*no context given*/]); |
||
| 475 | $default = $instance->packValue(); |
||
| 484 | |||
| 485 | /** |
||
| 486 | * Ensure default value for composite field, |
||
| 487 | * |
||
| 488 | * @param mixed $default |
||
| 489 | * @param CompositionDefinition $composition |
||
| 490 | * @param SchemaBuilder $builder |
||
| 491 | * |
||
| 492 | * @return array |
||
| 493 | * |
||
| 494 | * @throws SchemaException |
||
| 495 | */ |
||
| 496 | protected function compositionDefault( |
||
| 535 | } |
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.