| Total Complexity | 60 |
| Total Lines | 449 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like RelationsGeneratorTest 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.
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 RelationsGeneratorTest, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 28 | class RelationsGeneratorTest extends AbstractTest |
||
| 29 | { |
||
| 30 | public const WORK_DIR = AbstractTest::VAR_PATH . '/' . self::TEST_TYPE_LARGE . '/RelationsGeneratorTest/'; |
||
| 31 | |||
| 32 | public const TEST_ENTITY_BASKET = self::TEST_PROJECT_ROOT_NAMESPACE . '\\' |
||
| 33 | . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Basket'; |
||
| 34 | |||
| 35 | public const TEST_ENTITY_BASKET_ITEM = self::TEST_PROJECT_ROOT_NAMESPACE . '\\' |
||
| 36 | . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Basket\\Item'; |
||
| 37 | |||
| 38 | public const TEST_ENTITY_BASKET_ITEM_OFFER = self::TEST_PROJECT_ROOT_NAMESPACE . '\\' |
||
| 39 | . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Basket\\Item\\Offer'; |
||
| 40 | |||
| 41 | public const TEST_ENTITY_NESTED_THING = self::TEST_PROJECT_ROOT_NAMESPACE . '\\' |
||
| 42 | . AbstractGenerator::ENTITIES_FOLDER_NAME |
||
| 43 | . '\\GeneratedRelations\\Testing\\RelationsTestEntity'; |
||
| 44 | |||
| 45 | public const TEST_ENTITY_NESTED_THING2 = self::TEST_PROJECT_ROOT_NAMESPACE . '\\' |
||
| 46 | . AbstractGenerator::ENTITIES_FOLDER_NAME |
||
| 47 | . '\\GeneratedRelations\\ExtraTesting\\Test\\AnotherRelationsTestEntity'; |
||
| 48 | |||
| 49 | public const TEST_ENTITY_NAMESPACING_COMPANY = self::TEST_PROJECT_ROOT_NAMESPACE . '\\' |
||
| 50 | . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Company'; |
||
| 51 | |||
| 52 | public const TEST_ENTITY_NAMESPACING_SOME_CLIENT = self::TEST_PROJECT_ROOT_NAMESPACE . '\\' |
||
| 53 | . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Some\\Client'; |
||
| 54 | |||
| 55 | public const TEST_ENTITY_NAMESPACING_ANOTHER_CLIENT = self::TEST_PROJECT_ROOT_NAMESPACE . |
||
| 56 | '\\' |
||
| 57 | . |
||
| 58 | AbstractGenerator::ENTITIES_FOLDER_NAME . |
||
| 59 | '\\Another\\Client'; |
||
| 60 | |||
| 61 | public const TEST_ENTITIES = [ |
||
| 62 | self::TEST_ENTITY_BASKET, |
||
| 63 | self::TEST_ENTITY_BASKET_ITEM, |
||
| 64 | self::TEST_ENTITY_BASKET_ITEM_OFFER, |
||
| 65 | self::TEST_ENTITY_NESTED_THING, |
||
| 66 | self::TEST_ENTITY_NESTED_THING2, |
||
| 67 | ]; |
||
| 68 | |||
| 69 | public const TEST_ENTITIES_NAMESPACING = [ |
||
| 70 | self::TEST_ENTITY_NAMESPACING_COMPANY, |
||
| 71 | self::TEST_ENTITY_NAMESPACING_SOME_CLIENT, |
||
| 72 | self::TEST_ENTITY_NAMESPACING_ANOTHER_CLIENT, |
||
| 73 | ]; |
||
| 74 | protected static $buildOnce = true; |
||
| 75 | protected static $built = false; |
||
| 76 | /** |
||
| 77 | * @var EntityGenerator |
||
| 78 | */ |
||
| 79 | private $entityGenerator; |
||
| 80 | /** |
||
| 81 | * @var RelationsGenerator |
||
| 82 | */ |
||
| 83 | private $relationsGenerator; |
||
| 84 | /** |
||
| 85 | * @var ReflectionClass |
||
| 86 | */ |
||
| 87 | private $reflection; |
||
| 88 | /** |
||
| 89 | * @var string |
||
| 90 | */ |
||
| 91 | private $copiedExtraSuffix = ''; |
||
| 92 | |||
| 93 | /** |
||
| 94 | * @test |
||
| 95 | * @large |
||
| 96 | * @coversNothing |
||
| 97 | */ |
||
| 98 | public function allHasTypesInConstantArrays(): void |
||
| 99 | { |
||
| 100 | $hasTypes = []; |
||
| 101 | $constants = $this->getReflection()->getConstants(); |
||
| 102 | foreach ($constants as $constantName => $constantValue) { |
||
| 103 | if (0 === strpos($constantName, 'HAS') && false === strpos($constantName, 'HAS_TYPES')) { |
||
| 104 | $hasTypes[$constantName] = $constantValue; |
||
| 105 | } |
||
| 106 | } |
||
| 107 | $hasTypesCounted = count($hasTypes); |
||
| 108 | $hasTypesDefinedInConstantArray = count(RelationsGenerator::HAS_TYPES); |
||
| 109 | $fullDiff = static function (array $arrayX, array $arrayY): array { |
||
| 110 | $intersect = array_intersect($arrayX, $arrayY); |
||
| 111 | |||
| 112 | return array_merge(array_diff($arrayX, $intersect), array_diff($arrayY, $intersect)); |
||
| 113 | }; |
||
| 114 | self::assertSame( |
||
| 115 | $hasTypesCounted, |
||
| 116 | $hasTypesDefinedInConstantArray, |
||
| 117 | 'The number of defined in the constant array RelationsGenerator::HAS_TYPES is not correct:' |
||
| 118 | . " \n\nfull diff:\n " |
||
| 119 | . print_r($fullDiff($hasTypes, RelationsGenerator::HAS_TYPES), true) |
||
| 120 | ); |
||
| 121 | } |
||
| 122 | |||
| 123 | /** |
||
| 124 | * @return ReflectionClass |
||
| 125 | * @throws ReflectionException |
||
| 126 | */ |
||
| 127 | private function getReflection(): ReflectionClass |
||
| 128 | { |
||
| 129 | if (null === $this->reflection) { |
||
| 130 | $this->reflection = new ReflectionClass(RelationsGenerator::class); |
||
| 131 | } |
||
| 132 | |||
| 133 | return $this->reflection; |
||
| 134 | } |
||
| 135 | |||
| 136 | /** |
||
| 137 | * @test |
||
| 138 | * @throws DoctrineStaticMetaException |
||
| 139 | * @throws ReflectionException |
||
| 140 | */ |
||
| 141 | public function generateRelations(): void |
||
| 142 | { |
||
| 143 | /** |
||
| 144 | * @var SplFileInfo $i |
||
| 145 | */ |
||
| 146 | foreach (self::TEST_ENTITIES as $entityFqn) { |
||
| 147 | $entityFqn = $this->getCopiedFqn($entityFqn); |
||
| 148 | foreach ($this->relationsGenerator->getRelativePathRelationsGenerator() as $relativePath => $i) { |
||
| 149 | if ($i->isDir()) { |
||
| 150 | continue; |
||
| 151 | } |
||
| 152 | $entityRefl = new ReflectionClass($entityFqn); |
||
| 153 | $namespace = $entityRefl->getNamespaceName(); |
||
| 154 | $className = $entityRefl->getShortName(); |
||
| 155 | $namespaceNoEntities = substr( |
||
| 156 | $namespace, |
||
| 157 | strpos( |
||
| 158 | $namespace, |
||
| 159 | AbstractGenerator::ENTITIES_FOLDER_NAME |
||
| 160 | ) + strlen(AbstractGenerator::ENTITIES_FOLDER_NAME) |
||
| 161 | ); |
||
| 162 | $subPathNoEntites = str_replace('\\', '/', $namespaceNoEntities); |
||
| 163 | $plural = ucfirst($entityFqn::getDoctrineStaticMeta()->getPlural()); |
||
| 164 | $singular = ucfirst($entityFqn::getDoctrineStaticMeta()->getSingular()); |
||
| 165 | $relativePath = str_replace( |
||
| 166 | ['TemplateEntity', 'TemplateEntities'], |
||
| 167 | [$singular, $plural], |
||
| 168 | $relativePath |
||
| 169 | ); |
||
| 170 | $createdFile = realpath($this->copiedWorkDir) |
||
| 171 | . '/' . AbstractCommand::DEFAULT_SRC_SUBFOLDER |
||
| 172 | . '/' . AbstractGenerator::ENTITY_RELATIONS_FOLDER_NAME |
||
| 173 | . '/' . $subPathNoEntites . '/' |
||
| 174 | . $className . '/' . $relativePath; |
||
| 175 | $this->assertNoMissedReplacements($createdFile); |
||
| 176 | } |
||
| 177 | } |
||
| 178 | $this->qaGeneratedCode(); |
||
| 179 | } |
||
| 180 | |||
| 181 | /** |
||
| 182 | * @test |
||
| 183 | * @large |
||
| 184 | * * @throws ReflectionException |
||
| 185 | * @throws DoctrineStaticMetaException |
||
| 186 | */ |
||
| 187 | public function setRelationsBetweenEntities(): void |
||
| 188 | { |
||
| 189 | $errors = []; |
||
| 190 | foreach (RelationsGenerator::HAS_TYPES as $hasType) { |
||
| 191 | foreach ([true, false] as $requiredReciprocation) { |
||
| 192 | try { |
||
| 193 | if (false !== strpos($hasType, RelationsGenerator::PREFIX_INVERSE)) { |
||
| 194 | //inverse types are tested implicitly |
||
| 195 | continue; |
||
| 196 | } |
||
| 197 | $this->copiedExtraSuffix = |
||
| 198 | $hasType . ($requiredReciprocation ? 'RecipRequired' : 'RecipNotRequired'); |
||
| 199 | $this->setUp(); |
||
| 200 | |||
| 201 | $this->relationsGenerator->setEntityHasRelationToEntity( |
||
| 202 | $this->getCopiedFqn(self::TEST_ENTITY_BASKET), |
||
| 203 | $hasType, |
||
| 204 | $this->getCopiedFqn(self::TEST_ENTITY_BASKET_ITEM), |
||
| 205 | $requiredReciprocation |
||
| 206 | ); |
||
| 207 | $this->assertCorrectInterfacesSet( |
||
| 208 | $this->getCopiedFqn(self::TEST_ENTITY_BASKET), |
||
| 209 | $hasType, |
||
| 210 | $this->getCopiedFqn(self::TEST_ENTITY_BASKET_ITEM), |
||
| 211 | $requiredReciprocation |
||
| 212 | ); |
||
| 213 | |||
| 214 | $this->relationsGenerator->setEntityHasRelationToEntity( |
||
| 215 | $this->getCopiedFqn(self::TEST_ENTITY_BASKET), |
||
| 216 | $hasType, |
||
| 217 | $this->getCopiedFqn(self::TEST_ENTITY_BASKET_ITEM_OFFER), |
||
| 218 | $requiredReciprocation |
||
| 219 | ); |
||
| 220 | $this->assertCorrectInterfacesSet( |
||
| 221 | $this->getCopiedFqn(self::TEST_ENTITY_BASKET), |
||
| 222 | $hasType, |
||
| 223 | $this->getCopiedFqn(self::TEST_ENTITY_BASKET_ITEM_OFFER), |
||
| 224 | $requiredReciprocation |
||
| 225 | ); |
||
| 226 | |||
| 227 | $this->relationsGenerator->setEntityHasRelationToEntity( |
||
| 228 | $this->getCopiedFqn(self::TEST_ENTITY_NESTED_THING), |
||
| 229 | $hasType, |
||
| 230 | $this->getCopiedFqn(self::TEST_ENTITY_NESTED_THING2), |
||
| 231 | $requiredReciprocation |
||
| 232 | ); |
||
| 233 | $this->assertCorrectInterfacesSet( |
||
| 234 | $this->getCopiedFqn(self::TEST_ENTITY_NESTED_THING), |
||
| 235 | $hasType, |
||
| 236 | $this->getCopiedFqn(self::TEST_ENTITY_NESTED_THING2), |
||
| 237 | $requiredReciprocation |
||
| 238 | ); |
||
| 239 | $this->qaGeneratedCode(); |
||
| 240 | } catch (DoctrineStaticMetaException $e) { |
||
| 241 | $errors[] = [ |
||
| 242 | 'Failed setting relations using' => |
||
| 243 | [ |
||
| 244 | $this->getCopiedFqn(self::TEST_ENTITIES[0]), |
||
| 245 | $hasType, |
||
| 246 | $this->getCopiedFqn(self::TEST_ENTITIES[1]), |
||
| 247 | ], |
||
| 248 | 'Exception message' => $e->getMessage(), |
||
| 249 | 'Exception trace' => $e->getTraceAsStringRelativePath(), |
||
| 250 | ]; |
||
| 251 | } |
||
| 252 | } |
||
| 253 | } |
||
| 254 | self::assertEmpty( |
||
| 255 | $errors, |
||
| 256 | 'Found ' . count($errors) . ' errors: ' |
||
| 257 | . print_r($errors, true) |
||
| 258 | ); |
||
| 259 | $this->copiedExtraSuffix = null; |
||
| 260 | $this->copiedRootNamespace = null; |
||
| 261 | } |
||
| 262 | |||
| 263 | public function setUp() |
||
| 264 | { |
||
| 265 | parent::setUp(); |
||
| 266 | $this->entityGenerator = $this->getEntityGenerator(); |
||
| 267 | $this->relationsGenerator = $this->getRelationsGenerator(); |
||
| 268 | if (false === self::$built) { |
||
| 269 | foreach (self::TEST_ENTITIES as $fqn) { |
||
| 270 | $this->entityGenerator->generateEntity($fqn); |
||
| 271 | $this->relationsGenerator->generateRelationCodeForEntity($fqn); |
||
| 272 | } |
||
| 273 | self::$built = true; |
||
| 274 | } |
||
| 275 | $this->setupCopiedWorkDir(); |
||
| 276 | $this->relationsGenerator->setPathToProjectRoot($this->copiedWorkDir) |
||
| 277 | ->setProjectRootNamespace($this->copiedRootNamespace); |
||
| 278 | } |
||
| 279 | |||
| 280 | /** |
||
| 281 | * Inspect the generated class and ensure that all required interfaces have been implemented |
||
| 282 | * |
||
| 283 | * @param string $owningEntityFqn |
||
| 284 | * @param string $hasType |
||
| 285 | * @param string $ownedEntityFqn |
||
| 286 | * @param bool $requiredReciprocation |
||
| 287 | * @param bool $assertInverse |
||
| 288 | * |
||
| 289 | * @return void |
||
| 290 | * @throws ReflectionException |
||
| 291 | * @SuppressWarnings(PHPMD) |
||
| 292 | */ |
||
| 293 | private function assertCorrectInterfacesSet( |
||
| 294 | string $owningEntityFqn, |
||
| 295 | string $hasType, |
||
| 296 | string $ownedEntityFqn, |
||
| 297 | bool $requiredReciprocation, |
||
| 298 | bool $assertInverse = true |
||
| 299 | ): void { |
||
| 300 | $owningInterfaces = $this->getOwningEntityInterfaces($owningEntityFqn); |
||
| 301 | $expectedInterfaces = $this->getExpectedInterfacesForEntityFqn($ownedEntityFqn, $hasType); |
||
| 302 | |||
| 303 | $missingOwningInterfaces = []; |
||
| 304 | foreach ($expectedInterfaces as $expectedInterface) { |
||
| 305 | if (!in_array($expectedInterface, $owningInterfaces, true)) { |
||
| 306 | $missingOwningInterfaces[] = $expectedInterface; |
||
| 307 | } |
||
| 308 | } |
||
| 309 | self::assertEmpty( |
||
| 310 | $missingOwningInterfaces, |
||
| 311 | 'Entity ' . $owningEntityFqn . ' has some expected owning interfaces missing for hasType: ' |
||
| 312 | . $hasType . "\n\n" |
||
| 313 | . print_r($missingOwningInterfaces, true) |
||
| 314 | ); |
||
| 315 | |||
| 316 | if ($assertInverse) { |
||
| 317 | $inverseHasType = $this->getInverseHasType($hasType, $requiredReciprocation); |
||
| 318 | if (null === $inverseHasType) { |
||
| 319 | return; |
||
| 320 | } |
||
| 321 | $this->assertCorrectInterfacesSet( |
||
| 322 | $ownedEntityFqn, |
||
| 323 | $inverseHasType, |
||
| 324 | $owningEntityFqn, |
||
| 325 | false, |
||
| 326 | false |
||
| 327 | ); |
||
| 328 | } |
||
| 329 | } |
||
| 330 | |||
| 331 | /** |
||
| 332 | * Get an array of all the interfaces for a class |
||
| 333 | * |
||
| 334 | * @param string $classFqn |
||
| 335 | * |
||
| 336 | * @return array |
||
| 337 | * @throws ReflectionException |
||
| 338 | */ |
||
| 339 | private function getOwningEntityInterfaces(string $classFqn): array |
||
| 344 | } |
||
| 345 | |||
| 346 | /** |
||
| 347 | * Get a an array of interfaces (short names) by reading the PHP file itself |
||
| 348 | * |
||
| 349 | * @param string $classFilePath |
||
| 350 | * |
||
| 351 | * @return array |
||
| 352 | */ |
||
| 353 | private function getImplementedInterfacesFromClassFile(string $classFilePath): array |
||
| 373 | } |
||
| 374 | |||
| 375 | /** |
||
| 376 | * Get an array of the interfaces we expect an entity to implement based on the has type |
||
| 377 | * |
||
| 378 | * @param string $entityFqn |
||
| 379 | * @param string $hasType |
||
| 380 | * |
||
| 381 | * @return array |
||
| 382 | */ |
||
| 383 | private function getExpectedInterfacesForEntityFqn(string $entityFqn, string $hasType): array |
||
| 403 | } |
||
| 404 | |||
| 405 | /** |
||
| 406 | * Get the inverse has type name, or false if there is no inverse has type |
||
| 407 | * |
||
| 408 | * @param string $hasType |
||
| 409 | * |
||
| 410 | * @param bool $requiredReciprocation |
||
| 411 | * |
||
| 412 | * @return string|false |
||
| 413 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) |
||
| 414 | * @SuppressWarnings(PHPMD.NPathComplexity) |
||
| 415 | */ |
||
| 416 | private function getInverseHasType(string $hasType, bool $requiredReciprocation): ?string |
||
| 417 | { |
||
| 418 | $inverseHasType = null; |
||
| 419 | switch ($hasType) { |
||
| 420 | case RelationsGenerator::HAS_ONE_TO_ONE: |
||
| 421 | case RelationsGenerator::HAS_MANY_TO_MANY: |
||
| 422 | case RelationsGenerator::HAS_REQUIRED_ONE_TO_ONE: |
||
| 423 | case RelationsGenerator::HAS_REQUIRED_MANY_TO_MANY: |
||
| 424 | $inverseHasType = str_replace( |
||
| 425 | RelationsGenerator::PREFIX_OWNING, |
||
| 426 | RelationsGenerator::PREFIX_INVERSE, |
||
| 427 | $hasType |
||
| 428 | ); |
||
| 429 | break; |
||
| 430 | |||
| 431 | case RelationsGenerator::HAS_INVERSE_ONE_TO_ONE: |
||
| 432 | case RelationsGenerator::HAS_INVERSE_MANY_TO_MANY: |
||
| 433 | case RelationsGenerator::HAS_REQUIRED_INVERSE_ONE_TO_ONE: |
||
| 434 | case RelationsGenerator::HAS_REQUIRED_INVERSE_MANY_TO_MANY: |
||
| 435 | $inverseHasType = str_replace( |
||
| 436 | RelationsGenerator::PREFIX_INVERSE, |
||
| 437 | RelationsGenerator::PREFIX_OWNING, |
||
| 438 | $hasType |
||
| 439 | ); |
||
| 440 | break; |
||
| 441 | |||
| 442 | case RelationsGenerator::HAS_MANY_TO_ONE: |
||
| 443 | case RelationsGenerator::HAS_REQUIRED_MANY_TO_ONE: |
||
| 444 | $inverseHasType = RelationsGenerator::HAS_ONE_TO_MANY; |
||
| 445 | break; |
||
| 446 | case RelationsGenerator::HAS_ONE_TO_MANY: |
||
| 447 | case RelationsGenerator::HAS_REQUIRED_ONE_TO_MANY: |
||
| 448 | $inverseHasType = RelationsGenerator::HAS_MANY_TO_ONE; |
||
| 449 | break; |
||
| 450 | case RelationsGenerator::HAS_UNIDIRECTIONAL_ONE_TO_ONE: |
||
| 451 | case RelationsGenerator::HAS_UNIDIRECTIONAL_ONE_TO_MANY: |
||
| 452 | case RelationsGenerator::HAS_UNIDIRECTIONAL_MANY_TO_ONE: |
||
| 453 | case RelationsGenerator::HAS_REQUIRED_UNIDIRECTIONAL_ONE_TO_ONE: |
||
| 454 | case RelationsGenerator::HAS_REQUIRED_UNIDIRECTIONAL_ONE_TO_MANY: |
||
| 455 | case RelationsGenerator::HAS_REQUIRED_UNIDIRECTIONAL_MANY_TO_ONE: |
||
| 456 | return null; |
||
| 457 | default: |
||
| 458 | $this->fail('Failed getting $inverseHasType for $hasType ' . $hasType); |
||
| 459 | } |
||
| 460 | if (true === $requiredReciprocation && 0 === strpos($inverseHasType, RelationsGenerator::PREFIX_REQUIRED)) { |
||
| 461 | return $inverseHasType; |
||
| 462 | } |
||
| 463 | if (true === $requiredReciprocation && 0 !== strpos($inverseHasType, RelationsGenerator::PREFIX_REQUIRED)) { |
||
| 464 | return RelationsGenerator::PREFIX_REQUIRED . $inverseHasType; |
||
| 465 | } |
||
| 466 | if (false === $requiredReciprocation && 0 !== strpos($inverseHasType, RelationsGenerator::PREFIX_REQUIRED)) { |
||
| 467 | return $inverseHasType; |
||
| 468 | } |
||
| 469 | if (false === $requiredReciprocation && 0 === strpos($inverseHasType, RelationsGenerator::PREFIX_REQUIRED)) { |
||
| 470 | return substr($inverseHasType, 8); |
||
| 471 | } |
||
| 472 | } |
||
| 473 | |||
| 474 | protected function getCopiedNamespaceRoot(): string |
||
| 477 | } |
||
| 478 | } |
||
| 479 |