1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace EdmondsCommerce\DoctrineStaticMeta\Entity\Testing; |
||||
6 | |||||
7 | use Doctrine\DBAL\Exception\UniqueConstraintViolationException; |
||||
8 | use Doctrine\ORM\EntityManagerInterface; |
||||
9 | use Doctrine\ORM\Mapping\ClassMetadata; |
||||
10 | use Doctrine\ORM\Query\QueryException; |
||||
11 | use Doctrine\ORM\Tools\SchemaValidator; |
||||
12 | use Doctrine\ORM\Utility\PersisterHelper; |
||||
13 | use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\CodeHelper; |
||||
14 | use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator; |
||||
15 | use EdmondsCommerce\DoctrineStaticMeta\Config; |
||||
16 | use EdmondsCommerce\DoctrineStaticMeta\ConfigInterface; |
||||
17 | use EdmondsCommerce\DoctrineStaticMeta\Entity\DataTransferObjects\DtoFactory; |
||||
18 | use EdmondsCommerce\DoctrineStaticMeta\Entity\Embeddable\Objects\AbstractEmbeddableObject; |
||||
19 | use EdmondsCommerce\DoctrineStaticMeta\Entity\Fields\Interfaces\PrimaryKey\IdFieldInterface; |
||||
20 | use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\EntityInterface; |
||||
21 | use EdmondsCommerce\DoctrineStaticMeta\Entity\Repositories\RepositoryFactory; |
||||
22 | use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\EntitySaverFactory; |
||||
23 | use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\EntitySaverInterface; |
||||
24 | use EdmondsCommerce\DoctrineStaticMeta\Entity\Testing\EntityGenerator\TestEntityGenerator; |
||||
25 | use EdmondsCommerce\DoctrineStaticMeta\Entity\Testing\EntityGenerator\TestEntityGeneratorFactory; |
||||
26 | use EdmondsCommerce\DoctrineStaticMeta\Entity\Testing\Fixtures\AbstractEntityFixtureLoader; |
||||
27 | use EdmondsCommerce\DoctrineStaticMeta\Entity\Testing\Fixtures\FixturesHelper; |
||||
28 | use EdmondsCommerce\DoctrineStaticMeta\Entity\Testing\Fixtures\FixturesHelperFactory; |
||||
29 | use EdmondsCommerce\DoctrineStaticMeta\Exception\ConfigException; |
||||
30 | use EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException; |
||||
31 | use EdmondsCommerce\DoctrineStaticMeta\Exception\ValidationException; |
||||
32 | use EdmondsCommerce\DoctrineStaticMeta\MappingHelper; |
||||
33 | use EdmondsCommerce\DoctrineStaticMeta\SimpleEnv; |
||||
34 | use ErrorException; |
||||
35 | use Exception; |
||||
36 | use PHPUnit\Framework\TestCase; |
||||
37 | use Psr\Container\ContainerInterface; |
||||
38 | use ReflectionException; |
||||
39 | use ReflectionMethod; |
||||
40 | |||||
41 | use function stripos; |
||||
42 | use function substr; |
||||
43 | |||||
44 | /** |
||||
45 | * Class AbstractEntityTest |
||||
46 | * |
||||
47 | * This abstract test is designed to give you a good level of test coverage for your entities without any work required. |
||||
48 | * |
||||
49 | * You should extend the test with methods that test your specific business logic, your validators and anything else. |
||||
50 | * |
||||
51 | * You can override the methods, properties and constants as you see fit. |
||||
52 | * |
||||
53 | * @package EdmondsCommerce\DoctrineStaticMeta\Entity |
||||
54 | * @SuppressWarnings(PHPMD.CouplingBetweenObjects) |
||||
55 | * @SuppressWarnings(PHPMD.NumberOfChildren) |
||||
56 | * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) |
||||
57 | * @SuppressWarnings(PHPMD.TooManyPublicMethods) |
||||
58 | * @SuppressWarnings(PHPMD.StaticAccess) |
||||
59 | */ |
||||
60 | abstract class AbstractEntityTest extends TestCase implements EntityTestInterface |
||||
61 | { |
||||
62 | /** |
||||
63 | * @var ContainerInterface |
||||
64 | */ |
||||
65 | protected static $container; |
||||
66 | /** |
||||
67 | * The fully qualified name of the Entity being tested, as calculated by the test class name |
||||
68 | * |
||||
69 | * @var string |
||||
70 | */ |
||||
71 | protected static $testedEntityFqn; |
||||
72 | |||||
73 | /** |
||||
74 | * @var TestEntityGenerator |
||||
75 | */ |
||||
76 | protected static $testEntityGenerator; |
||||
77 | |||||
78 | /** |
||||
79 | * @var TestEntityGeneratorFactory |
||||
80 | */ |
||||
81 | protected static $testEntityGeneratorFactory; |
||||
82 | |||||
83 | /** |
||||
84 | * @var array |
||||
85 | */ |
||||
86 | protected static $schemaErrors = []; |
||||
87 | |||||
88 | public static function setUpBeforeClass(): void |
||||
89 | { |
||||
90 | if (null !== static::$container) { |
||||
91 | self::tearDownAfterClass(); |
||||
92 | } |
||||
93 | static::initContainer(); |
||||
94 | static::$testedEntityFqn = substr(static::class, 0, -4); |
||||
95 | static::$testEntityGeneratorFactory = static::$container->get(TestEntityGeneratorFactory::class); |
||||
96 | static::$testEntityGenerator = |
||||
97 | static::$testEntityGeneratorFactory->createForEntityFqn(static::$testedEntityFqn); |
||||
98 | } |
||||
99 | |||||
100 | public static function tearDownAfterClass(): void |
||||
101 | { |
||||
102 | $entityManager = static::$container->get(EntityManagerInterface::class); |
||||
103 | $entityManager->close(); |
||||
104 | $entityManager->getConnection()->close(); |
||||
105 | self::$container = null; |
||||
106 | static::$container = null; |
||||
107 | } |
||||
108 | |||||
109 | public static function initContainer(): void |
||||
110 | { |
||||
111 | $testConfig = self::getTestContainerConfig(); |
||||
112 | static::$container = TestContainerFactory::getContainer($testConfig); |
||||
113 | } |
||||
114 | |||||
115 | /** |
||||
116 | * @throws ConfigException |
||||
117 | * @throws DoctrineStaticMetaException |
||||
118 | * @SuppressWarnings(PHPMD.Superglobals) |
||||
119 | * @SuppressWarnings(PHPMD.StaticAccess) |
||||
120 | */ |
||||
121 | public static function getTestContainerConfig(): array |
||||
122 | { |
||||
123 | SimpleEnv::setEnv(Config::getProjectRootDirectory() . '/.env'); |
||||
124 | $testConfig = $_SERVER; |
||||
125 | if (preg_match('%_test$%m', $_SERVER[ConfigInterface::PARAM_DB_NAME]) !== 1) { |
||||
126 | $testConfig[ConfigInterface::PARAM_DB_NAME] = $_SERVER[ConfigInterface::PARAM_DB_NAME] . '_test'; |
||||
127 | } |
||||
128 | |||||
129 | return $testConfig; |
||||
130 | } |
||||
131 | |||||
132 | /** |
||||
133 | * This test checks that the fixtures can be loaded properly |
||||
134 | * |
||||
135 | * The test returns the array of fixtures which means you could choose to add a test that `depends` on this test |
||||
136 | * |
||||
137 | * @test |
||||
138 | */ |
||||
139 | public function theFixtureCanBeLoaded(): array |
||||
140 | { |
||||
141 | /** |
||||
142 | * @var FixturesHelper $fixtureHelper |
||||
143 | */ |
||||
144 | $fixtureHelper = $this->getFixturesHelper(); |
||||
145 | /** |
||||
146 | * This can seriously hurt performance, but is needed as a default |
||||
147 | */ |
||||
148 | $fixtureHelper->setLoadFromCache(false); |
||||
149 | /** |
||||
150 | * @var AbstractEntityFixtureLoader $fixture |
||||
151 | */ |
||||
152 | $fixture = $fixtureHelper->createFixtureInstanceForEntityFqn(static::$testedEntityFqn); |
||||
153 | $fixtureHelper->createDb($fixture); |
||||
154 | $loaded = $this->loadAllEntities(); |
||||
155 | $expectedAmountLoaded = $fixture::BULK_AMOUNT_TO_GENERATE; |
||||
156 | $actualAmountLoaded = count($loaded); |
||||
157 | self::assertGreaterThanOrEqual( |
||||
158 | $expectedAmountLoaded, |
||||
159 | $actualAmountLoaded, |
||||
160 | "expected to load at least $expectedAmountLoaded but only loaded $actualAmountLoaded" |
||||
161 | ); |
||||
162 | |||||
163 | return $loaded; |
||||
164 | } |
||||
165 | |||||
166 | /** |
||||
167 | * Test that we have correctly generated an instance of our test entity |
||||
168 | * |
||||
169 | * @param array $fixtureEntities |
||||
170 | * |
||||
171 | * @return EntityInterface |
||||
172 | * @SuppressWarnings(PHPMD.StaticAccess) |
||||
173 | * @depends theFixtureCanBeLoaded |
||||
174 | * @test |
||||
175 | */ |
||||
176 | public function weCanGenerateANewEntityInstance(array $fixtureEntities): EntityInterface |
||||
177 | { |
||||
178 | $generated = current($fixtureEntities); |
||||
179 | self::assertInstanceOf(static::$testedEntityFqn, $generated); |
||||
180 | |||||
181 | return $generated; |
||||
182 | } |
||||
183 | |||||
184 | /** |
||||
185 | * @test |
||||
186 | * Use Doctrine's built in schema validation tool to catch issues |
||||
187 | */ |
||||
188 | public function theSchemaIsValidForThisEntity() |
||||
189 | { |
||||
190 | $errors = $this->getSchemaErrors(); |
||||
191 | $message = ''; |
||||
192 | if (isset($errors[static::$testedEntityFqn])) { |
||||
193 | $message = "Failed ORM Validate Schema:\n"; |
||||
194 | foreach ($errors[static::$testedEntityFqn] as $err) { |
||||
195 | $message .= "\n * $err \n"; |
||||
196 | } |
||||
197 | } |
||||
198 | self::assertEmpty($message, $message); |
||||
199 | } |
||||
200 | |||||
201 | /** |
||||
202 | * Loop through Entity fields, call the getter and where possible assert there is a value returned |
||||
203 | * |
||||
204 | * @param EntityInterface $entity |
||||
205 | * |
||||
206 | * @return EntityInterface |
||||
207 | * @throws QueryException |
||||
208 | * @throws ReflectionException |
||||
209 | * @SuppressWarnings(PHPMD.StaticAccess) |
||||
210 | * @test |
||||
211 | * @depends weCanGenerateANewEntityInstance |
||||
212 | */ |
||||
213 | public function theEntityGettersReturnValues(EntityInterface $entity): EntityInterface |
||||
214 | { |
||||
215 | $meta = $this->getEntityManager()->getClassMetadata(static::$testedEntityFqn); |
||||
216 | $dto = $this->getDtoFactory()->createDtoFromEntity($entity); |
||||
217 | foreach ($meta->getFieldNames() as $fieldName) { |
||||
218 | if ('id' === $fieldName) { |
||||
219 | continue; |
||||
220 | } |
||||
221 | $type = PersisterHelper::getTypeOfField($fieldName, $meta, $this->getEntityManager())[0]; |
||||
222 | $method = $this->getGetterNameForField($fieldName, $type); |
||||
223 | if (\ts\stringContains($method, '.')) { |
||||
224 | [$getEmbeddableMethod,] = explode('.', $method); |
||||
225 | $embeddable = $entity->$getEmbeddableMethod(); |
||||
226 | self::assertInstanceOf(AbstractEmbeddableObject::class, $embeddable); |
||||
227 | continue; |
||||
228 | } |
||||
229 | $reflectionMethod = new ReflectionMethod($entity, $method); |
||||
230 | if ($reflectionMethod->hasReturnType()) { |
||||
231 | $returnType = $reflectionMethod->getReturnType(); |
||||
232 | $allowsNull = $returnType->allowsNull(); |
||||
233 | if ($allowsNull) { |
||||
234 | // As we can't assert anything here so simply call |
||||
235 | // the method and allow the type hint to raise any |
||||
236 | // errors. |
||||
237 | $entity->$method(); |
||||
238 | continue; |
||||
239 | } |
||||
240 | self::assertNotNull($dto->$method(), "$fieldName getter returned null"); |
||||
241 | continue; |
||||
242 | } |
||||
243 | // If there is no return type then we can't assert anything, |
||||
244 | // but again we can just call the getter to check for errors |
||||
245 | $dto->$method(); |
||||
246 | } |
||||
247 | if (0 === $this->getCount()) { |
||||
248 | self::assertTrue(true); |
||||
249 | } |
||||
250 | |||||
251 | return $entity; |
||||
252 | } |
||||
253 | |||||
254 | /** |
||||
255 | * @param EntityInterface $generated |
||||
256 | * |
||||
257 | * @return EntityInterface |
||||
258 | * @test |
||||
259 | * @depends theEntityGettersReturnValues |
||||
260 | * @throws ErrorException |
||||
261 | */ |
||||
262 | public function weCanExtendTheEntityWithUnrequiredAssociationEntities(EntityInterface $generated): EntityInterface |
||||
263 | { |
||||
264 | if ([] === $this->getTestedEntityClassMetaData()->getAssociationMappings()) { |
||||
265 | $this->markTestSkipped('No associations to test'); |
||||
266 | } |
||||
267 | $this->getTestEntityGenerator()->addAssociationEntities($generated); |
||||
268 | $this->assertAllAssociationsAreNotEmpty($generated); |
||||
269 | |||||
270 | return $generated; |
||||
271 | } |
||||
272 | |||||
273 | /** |
||||
274 | * @param EntityInterface $generated |
||||
275 | * |
||||
276 | * @return EntityInterface |
||||
277 | * @throws DoctrineStaticMetaException |
||||
278 | * @throws ReflectionException |
||||
279 | * @test |
||||
280 | * @depends weCanExtendTheEntityWithUnrequiredAssociationEntities |
||||
281 | */ |
||||
282 | public function theEntityCanBeSavedAndReloadedFromTheDatabase(EntityInterface $generated): EntityInterface |
||||
283 | { |
||||
284 | $this->getEntitySaver()->save($generated); |
||||
285 | $loaded = $this->loadEntity($generated->getId()); |
||||
286 | self::assertSame((string)$generated->getId(), (string)$loaded->getId()); |
||||
287 | self::assertInstanceOf(static::$testedEntityFqn, $loaded); |
||||
288 | |||||
289 | return $loaded; |
||||
290 | } |
||||
291 | |||||
292 | /** |
||||
293 | * @param EntityInterface $loaded |
||||
294 | * |
||||
295 | * @return EntityInterface |
||||
296 | * @throws DoctrineStaticMetaException |
||||
297 | * @throws ReflectionException |
||||
298 | * @throws ValidationException |
||||
299 | * @depends theEntityCanBeSavedAndReloadedFromTheDatabase |
||||
300 | * @test |
||||
301 | */ |
||||
302 | public function theLoadedEntityCanBeUpdatedAndResaved(EntityInterface $loaded): EntityInterface |
||||
303 | { |
||||
304 | $this->updateEntityFields($loaded); |
||||
305 | $this->assertAllAssociationsAreNotEmpty($loaded); |
||||
306 | $this->removeAllAssociations($loaded); |
||||
307 | $this->getEntitySaver()->save($loaded); |
||||
308 | |||||
309 | return $loaded; |
||||
310 | } |
||||
311 | |||||
312 | /** |
||||
313 | * @param EntityInterface $entity |
||||
314 | * |
||||
315 | * @return EntityInterface |
||||
316 | * @depends theLoadedEntityCanBeUpdatedAndResaved |
||||
317 | * @test |
||||
318 | */ |
||||
319 | public function theReloadedEntityHasNoAssociatedEntities(EntityInterface $entity): EntityInterface |
||||
320 | { |
||||
321 | $reLoaded = $this->loadEntity($entity->getId()); |
||||
322 | $entityDump = $this->dump($entity); |
||||
323 | $reLoadedDump = $this->dump($reLoaded); |
||||
324 | self::assertEquals($entityDump, $reLoadedDump); |
||||
325 | $this->assertAllAssociationsAreEmpty($reLoaded); |
||||
326 | |||||
327 | return $reLoaded; |
||||
328 | } |
||||
329 | |||||
330 | /** |
||||
331 | * @depends weCanGenerateANewEntityInstance |
||||
332 | * |
||||
333 | * @param EntityInterface $entity |
||||
334 | * |
||||
335 | * @throws ReflectionException |
||||
336 | * @test |
||||
337 | */ |
||||
338 | public function checkAllGettersCanBeReturnedFromDoctrineStaticMeta(EntityInterface $entity) |
||||
339 | { |
||||
340 | $getters = $entity::getDoctrineStaticMeta()->getGetters(); |
||||
341 | self::assertNotEmpty($getters); |
||||
342 | foreach ($getters as $getter) { |
||||
343 | self::assertRegExp('%^(get|is|has).+%', $getter); |
||||
344 | } |
||||
345 | } |
||||
346 | |||||
347 | /** |
||||
348 | * @depends weCanGenerateANewEntityInstance |
||||
349 | * @test |
||||
350 | * |
||||
351 | * @param EntityInterface $entity |
||||
352 | * |
||||
353 | * @throws ReflectionException |
||||
354 | */ |
||||
355 | public function checkAllSettersCanBeReturnedFromDoctrineStaticMeta(EntityInterface $entity) |
||||
356 | { |
||||
357 | $setters = $entity::getDoctrineStaticMeta()->getSetters(); |
||||
358 | self::assertNotEmpty($setters); |
||||
359 | foreach ($setters as $setter) { |
||||
360 | self::assertRegExp('%^(set|add).+%', $setter); |
||||
361 | } |
||||
362 | } |
||||
363 | |||||
364 | /** |
||||
365 | * Loop through entity fields and find unique ones |
||||
366 | * |
||||
367 | * Then ensure that the unique rule is being enforced as expected |
||||
368 | * |
||||
369 | * @throws ReflectionException |
||||
370 | * @throws ValidationException |
||||
371 | * @test |
||||
372 | * @depends theReloadedEntityHasNoAssociatedEntities |
||||
373 | */ |
||||
374 | public function checkThatWeCanNotSaveEntitiesWithDuplicateUniqueFieldValues(): void |
||||
375 | { |
||||
376 | $meta = $this->getTestedEntityClassMetaData(); |
||||
377 | $uniqueFields = []; |
||||
378 | foreach ($meta->getFieldNames() as $fieldName) { |
||||
379 | if (IdFieldInterface::PROP_ID === $fieldName) { |
||||
380 | continue; |
||||
381 | } |
||||
382 | if (true === $this->isUniqueField($fieldName)) { |
||||
383 | $uniqueFields[] = $fieldName; |
||||
384 | } |
||||
385 | } |
||||
386 | if ([] === $uniqueFields) { |
||||
387 | self::markTestSkipped('No unique fields to check'); |
||||
388 | |||||
389 | return; |
||||
390 | } |
||||
391 | foreach ($uniqueFields as $fieldName) { |
||||
392 | $primary = $this->getTestEntityGenerator()->generateEntity(); |
||||
393 | $secondary = $this->getTestEntityGenerator()->generateEntity(); |
||||
394 | $secondaryDto = $this->getDtoFactory()->createDtoFromEntity($secondary); |
||||
395 | $getter = 'get' . $fieldName; |
||||
396 | $setter = 'set' . $fieldName; |
||||
397 | $primaryValue = $primary->$getter(); |
||||
398 | $secondaryDto->$setter($primaryValue); |
||||
399 | $secondary->update($secondaryDto); |
||||
400 | $saver = $this->getEntitySaver(); |
||||
401 | $this->expectException(UniqueConstraintViolationException::class); |
||||
402 | $saver->saveAll([$primary, $secondary]); |
||||
403 | } |
||||
404 | } |
||||
405 | |||||
406 | protected function getFixturesHelper(): FixturesHelper |
||||
407 | { |
||||
408 | return static::$container->get(FixturesHelperFactory::class)->getFixturesHelper(); |
||||
409 | } |
||||
410 | |||||
411 | protected function loadAllEntities(): array |
||||
412 | { |
||||
413 | return static::$container->get(RepositoryFactory::class) |
||||
414 | ->getRepository(static::$testedEntityFqn) |
||||
415 | ->findAll(); |
||||
416 | } |
||||
417 | |||||
418 | /** |
||||
419 | * Use Doctrine's standard schema validation to get errors for the whole schema |
||||
420 | * |
||||
421 | * We cache this as a class property because the schema only needs validating once as a whole, after that we can |
||||
422 | * pull out Entity specific issues as required |
||||
423 | * |
||||
424 | * @param bool $update |
||||
425 | * |
||||
426 | * @return array |
||||
427 | * @throws Exception |
||||
428 | * @SuppressWarnings(PHPMD.BooleanArgumentFlag) |
||||
429 | */ |
||||
430 | protected function getSchemaErrors(bool $update = false): array |
||||
431 | { |
||||
432 | if ([] === static::$schemaErrors || true === $update) { |
||||
433 | $validator = new SchemaValidator($this->getEntityManager()); |
||||
434 | static::$schemaErrors = $validator->validateMapping(); |
||||
435 | } |
||||
436 | |||||
437 | return static::$schemaErrors; |
||||
438 | } |
||||
439 | |||||
440 | protected function getEntityManager(): EntityManagerInterface |
||||
441 | { |
||||
442 | return static::$container->get(EntityManagerInterface::class); |
||||
443 | } |
||||
444 | |||||
445 | protected function getDtoFactory(): DtoFactory |
||||
446 | { |
||||
447 | return static::$container->get(DtoFactory::class); |
||||
448 | } |
||||
449 | |||||
450 | protected function getGetterNameForField(string $fieldName, string $type): string |
||||
451 | { |
||||
452 | if ($type === 'boolean') { |
||||
453 | return static::$container->get(CodeHelper::class)->getGetterMethodNameForBoolean($fieldName); |
||||
454 | } |
||||
455 | |||||
456 | return 'get' . $fieldName; |
||||
457 | } |
||||
458 | |||||
459 | protected function getTestedEntityClassMetaData(): ClassMetadata |
||||
460 | { |
||||
461 | return $this->getEntityManager()->getClassMetadata(static::$testedEntityFqn); |
||||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||||
462 | } |
||||
463 | |||||
464 | protected function getTestEntityGenerator(): TestEntityGenerator |
||||
465 | { |
||||
466 | return static::$testEntityGenerator; |
||||
467 | } |
||||
468 | |||||
469 | protected function assertAllAssociationsAreNotEmpty(EntityInterface $entity) |
||||
470 | { |
||||
471 | $meta = $this->getTestedEntityClassMetaData(); |
||||
472 | foreach ($meta->getAssociationMappings() as $mapping) { |
||||
473 | $getter = 'get' . $mapping['fieldName']; |
||||
474 | if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) { |
||||
475 | $collection = $entity->$getter()->toArray(); |
||||
476 | $this->assertCorrectMappings(static::$testedEntityFqn, $mapping); |
||||
477 | self::assertNotEmpty( |
||||
478 | $collection, |
||||
479 | 'Failed to load the collection of the associated entity [' . $mapping['fieldName'] |
||||
480 | . '] from the generated ' . static::$testedEntityFqn |
||||
481 | . ', make sure you have reciprocal adding of the association' |
||||
482 | ); |
||||
483 | |||||
484 | continue; |
||||
485 | } |
||||
486 | $association = $entity->$getter(); |
||||
487 | self::assertNotEmpty( |
||||
488 | $association, |
||||
489 | 'Failed to load the associated entity: [' . $mapping['fieldName'] |
||||
490 | . '] from the generated ' . static::$testedEntityFqn |
||||
491 | ); |
||||
492 | self::assertNotEmpty( |
||||
493 | $association->getId(), |
||||
494 | 'Failed to get the ID of the associated entity: [' . $mapping['fieldName'] |
||||
495 | . '] from the generated ' . static::$testedEntityFqn |
||||
496 | ); |
||||
497 | } |
||||
498 | } |
||||
499 | |||||
500 | /** |
||||
501 | * Check the mapping of our class and the associated entity to make sure it's configured properly on both sides. |
||||
502 | * Very easy to get wrong. This is in addition to the standard Schema Validation |
||||
503 | * |
||||
504 | * @param string $classFqn |
||||
505 | * @param array $mapping |
||||
506 | */ |
||||
507 | protected function assertCorrectMappings(string $classFqn, array $mapping) |
||||
508 | { |
||||
509 | $entityManager = $this->getEntityManager(); |
||||
510 | $pass = false; |
||||
511 | $associationFqn = $mapping['targetEntity']; |
||||
512 | $associationMeta = $entityManager->getClassMetadata($associationFqn); |
||||
513 | $classTraits = $entityManager->getClassMetadata($classFqn) |
||||
514 | ->getReflectionClass() |
||||
515 | ->getTraits(); |
||||
516 | $unidirectionalTraitShortNamePrefixes = [ |
||||
517 | 'Has' . $associationFqn::getDoctrineStaticMeta()->getSingular() . RelationsGenerator::PREFIX_UNIDIRECTIONAL, |
||||
518 | 'Has' . $associationFqn::getDoctrineStaticMeta()->getPlural() . RelationsGenerator::PREFIX_UNIDIRECTIONAL, |
||||
519 | 'Has' . RelationsGenerator::PREFIX_REQUIRED . |
||||
520 | $associationFqn::getDoctrineStaticMeta()->getSingular() . RelationsGenerator::PREFIX_UNIDIRECTIONAL, |
||||
521 | 'Has' . RelationsGenerator::PREFIX_REQUIRED . |
||||
522 | $associationFqn::getDoctrineStaticMeta()->getPlural() . RelationsGenerator::PREFIX_UNIDIRECTIONAL, |
||||
523 | ]; |
||||
524 | foreach ($classTraits as $trait) { |
||||
525 | foreach ($unidirectionalTraitShortNamePrefixes as $namePrefix) { |
||||
526 | if (0 === stripos($trait->getShortName(), $namePrefix)) { |
||||
527 | return; |
||||
528 | } |
||||
529 | } |
||||
530 | } |
||||
531 | foreach ($associationMeta->getAssociationMappings() as $associationMapping) { |
||||
0 ignored issues
–
show
The method
getAssociationMappings() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata . Did you maybe mean getAssociationNames() ?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
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. ![]() |
|||||
532 | if ($classFqn === $associationMapping['targetEntity']) { |
||||
533 | $pass = $this->assertCorrectMapping($mapping, $associationMapping, $classFqn); |
||||
534 | break; |
||||
535 | } |
||||
536 | } |
||||
537 | self::assertTrue( |
||||
538 | $pass, |
||||
539 | 'Failed finding association mapping to test for ' . "\n" . $mapping['targetEntity'] |
||||
540 | ); |
||||
541 | } |
||||
542 | |||||
543 | /** |
||||
544 | * @param array $mapping |
||||
545 | * @param array $associationMapping |
||||
546 | * @param string $classFqn |
||||
547 | * |
||||
548 | * @return bool |
||||
549 | */ |
||||
550 | protected function assertCorrectMapping(array $mapping, array $associationMapping, string $classFqn): bool |
||||
551 | { |
||||
552 | if (empty($mapping['joinTable'])) { |
||||
553 | self::assertArrayNotHasKey( |
||||
554 | 'joinTable', |
||||
555 | $associationMapping, |
||||
556 | $classFqn . ' join table is empty, |
||||
557 | but association ' . $mapping['targetEntity'] . ' join table is not empty' |
||||
558 | ); |
||||
559 | |||||
560 | return true; |
||||
561 | } |
||||
562 | self::assertNotEmpty( |
||||
563 | $associationMapping['joinTable'], |
||||
564 | "$classFqn joinTable is set to " . $mapping['joinTable']['name'] |
||||
565 | . " \n association " . $mapping['targetEntity'] . ' join table is empty' |
||||
566 | ); |
||||
567 | self::assertSame( |
||||
568 | $mapping['joinTable']['name'], |
||||
569 | $associationMapping['joinTable']['name'], |
||||
570 | "join tables not the same: \n * $classFqn = " . $mapping['joinTable']['name'] |
||||
571 | . " \n * association " . $mapping['targetEntity'] |
||||
572 | . ' = ' . $associationMapping['joinTable']['name'] |
||||
573 | ); |
||||
574 | self::assertArrayHasKey( |
||||
575 | 'inverseJoinColumns', |
||||
576 | $associationMapping['joinTable'], |
||||
577 | "join table join columns not the same: \n * $classFqn joinColumn = " |
||||
578 | . $mapping['joinTable']['joinColumns'][0]['name'] |
||||
579 | . " \n * association " . $mapping['targetEntity'] |
||||
580 | . ' inverseJoinColumn is not set' |
||||
581 | ); |
||||
582 | self::assertSame( |
||||
583 | $mapping['joinTable']['joinColumns'][0]['name'], |
||||
584 | $associationMapping['joinTable']['inverseJoinColumns'][0]['name'], |
||||
585 | "join table join columns not the same: \n * $classFqn joinColumn = " |
||||
586 | . $mapping['joinTable']['joinColumns'][0]['name'] |
||||
587 | . " \n * association " . $mapping['targetEntity'] |
||||
588 | . ' inverseJoinColumn = ' . $associationMapping['joinTable']['inverseJoinColumns'][0]['name'] |
||||
589 | ); |
||||
590 | self::assertSame( |
||||
591 | $mapping['joinTable']['inverseJoinColumns'][0]['name'], |
||||
592 | $associationMapping['joinTable']['joinColumns'][0]['name'], |
||||
593 | "join table join columns not the same: \n * $classFqn inverseJoinColumn = " |
||||
594 | . $mapping['joinTable']['inverseJoinColumns'][0]['name'] |
||||
595 | . " \n * association " . $mapping['targetEntity'] . ' joinColumn = ' |
||||
596 | . $associationMapping['joinTable']['joinColumns'][0]['name'] |
||||
597 | ); |
||||
598 | |||||
599 | return true; |
||||
600 | } |
||||
601 | |||||
602 | protected function getEntitySaver(): EntitySaverInterface |
||||
603 | { |
||||
604 | return static::$container->get(EntitySaverFactory::class)->getSaverForEntityFqn(static::$testedEntityFqn); |
||||
605 | } |
||||
606 | |||||
607 | /** |
||||
608 | * @param mixed $id |
||||
609 | * |
||||
610 | * @return EntityInterface|null |
||||
611 | */ |
||||
612 | protected function loadEntity($id): EntityInterface |
||||
613 | { |
||||
614 | return static::$container->get(RepositoryFactory::class) |
||||
615 | ->getRepository(static::$testedEntityFqn) |
||||
616 | ->get($id); |
||||
617 | } |
||||
618 | |||||
619 | /** |
||||
620 | * Generate a new entity and then update our Entity with the values from the generated one |
||||
621 | * |
||||
622 | * @param EntityInterface $entity |
||||
623 | * |
||||
624 | * @throws ValidationException |
||||
625 | * @SuppressWarnings(PHPMD.StaticAccess) |
||||
626 | */ |
||||
627 | protected function updateEntityFields(EntityInterface $entity): void |
||||
628 | { |
||||
629 | $dto = $this->getDtoFactory()->createDtoFromEntity($entity); |
||||
630 | $this->getTestEntityGenerator()->fakerUpdateDto($dto); |
||||
631 | $entity->update($dto); |
||||
632 | } |
||||
633 | |||||
634 | /** |
||||
635 | * @param EntityInterface $entity |
||||
636 | * |
||||
637 | * @throws ReflectionException |
||||
638 | * @SuppressWarnings(PHPMD.StaticAccess) |
||||
639 | */ |
||||
640 | protected function removeAllAssociations(EntityInterface $entity) |
||||
641 | { |
||||
642 | $required = $entity::getDoctrineStaticMeta()->getRequiredRelationProperties(); |
||||
643 | $meta = $this->getTestedEntityClassMetaData(); |
||||
644 | $identifiers = array_flip($meta->getIdentifier()); |
||||
645 | foreach ($meta->getAssociationMappings() as $mapping) { |
||||
646 | if (isset($identifiers[$mapping['fieldName']])) { |
||||
647 | continue; |
||||
648 | } |
||||
649 | if (isset($required[$mapping['fieldName']])) { |
||||
650 | continue; |
||||
651 | } |
||||
652 | $remover = 'remove' . MappingHelper::singularize($mapping['fieldName']); |
||||
653 | if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) { |
||||
654 | $getter = 'get' . $mapping['fieldName']; |
||||
655 | $relations = $entity->$getter(); |
||||
656 | foreach ($relations as $relation) { |
||||
657 | $this->initialiseEntity($relation); |
||||
658 | $entity->$remover($relation); |
||||
659 | } |
||||
660 | continue; |
||||
661 | } |
||||
662 | $entity->$remover(); |
||||
663 | } |
||||
664 | $this->assertAllAssociationsAreEmpty($entity); |
||||
665 | } |
||||
666 | |||||
667 | protected function initialiseEntity(EntityInterface $entity): void |
||||
668 | { |
||||
669 | static::$testEntityGeneratorFactory |
||||
670 | ->createForEntityFqn($entity::getEntityFqn()) |
||||
671 | ->getEntityFactory() |
||||
672 | ->initialiseEntity($entity); |
||||
673 | } |
||||
674 | |||||
675 | protected function assertAllAssociationsAreEmpty(EntityInterface $entity) |
||||
676 | { |
||||
677 | $required = $entity::getDoctrineStaticMeta()->getRequiredRelationProperties(); |
||||
678 | $meta = $this->getTestedEntityClassMetaData(); |
||||
679 | $identifiers = array_flip($meta->getIdentifier()); |
||||
680 | foreach ($meta->getAssociationMappings() as $mapping) { |
||||
681 | if (isset($identifiers[$mapping['fieldName']])) { |
||||
682 | continue; |
||||
683 | } |
||||
684 | if (isset($required[$mapping['fieldName']])) { |
||||
685 | continue; |
||||
686 | } |
||||
687 | |||||
688 | $getter = 'get' . $mapping['fieldName']; |
||||
689 | if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) { |
||||
690 | $collection = $entity->$getter()->toArray(); |
||||
691 | self::assertEmpty( |
||||
692 | $collection, |
||||
693 | 'Collection of the associated entity [' . $mapping['fieldName'] |
||||
694 | . '] is not empty after calling remove' |
||||
695 | ); |
||||
696 | continue; |
||||
697 | } |
||||
698 | $association = $entity->$getter(); |
||||
699 | self::assertEmpty( |
||||
700 | $association, |
||||
701 | 'Failed to remove associated entity: [' . $mapping['fieldName'] |
||||
702 | . '] from the generated ' . static::$testedEntityFqn |
||||
703 | ); |
||||
704 | } |
||||
705 | } |
||||
706 | |||||
707 | protected function dump(EntityInterface $entity): string |
||||
708 | { |
||||
709 | return (new EntityDebugDumper())->dump($entity, $this->getEntityManager()); |
||||
710 | } |
||||
711 | |||||
712 | protected function isUniqueField(string $fieldName): bool |
||||
713 | { |
||||
714 | $fieldMapping = $this->getTestedEntityClassMetaData()->getFieldMapping($fieldName); |
||||
715 | |||||
716 | return array_key_exists('unique', $fieldMapping) && true === $fieldMapping['unique']; |
||||
717 | } |
||||
718 | } |
||||
719 |