GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#154)
by joseph
26:44
created

AbstractEntityTest   F

Complexity

Total Complexity 70

Size/Duplication

Total Lines 638
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 267
dl 0
loc 638
ccs 0
cts 407
cp 0
rs 2.8
c 0
b 0
f 0
wmc 70

32 Methods

Rating   Name   Duplication   Size   Complexity  
A setUpBeforeClass() 0 9 2
A initContainer() 0 4 1
A getTestContainerConfig() 0 7 1
A tearDownAfterClass() 0 7 1
A updateEntityFields() 0 5 1
A getGetterNameForField() 0 7 2
A isUniqueField() 0 5 2
A weCanExtendTheEntityWithUnrequiredAssociationEntities() 0 9 2
A getDtoFactory() 0 3 1
A removeAllAssociations() 0 24 6
B checkThatWeCanNotSaveEntitiesWithDuplicateUniqueFieldValues() 0 29 6
A theEntityCanBeSavedAndReloadedFromTheDatabase() 0 8 1
A loadAllEntities() 0 5 1
B assertCorrectMappings() 0 33 6
A getEntityManager() 0 3 1
A assertAllAssociationsAreNotEmpty() 0 27 3
A assertCorrectMapping() 0 50 2
A theLoadedEntityCanBeUpdatedAndResaved() 0 8 1
A checkAllGettersCanBeReturnedFromDoctrineStaticMeta() 0 6 2
A checkAllSettersCanBeReturnedFromDoctrineStaticMeta() 0 6 2
A loadEntity() 0 5 1
A theFixtureCanBeLoaded() 0 25 1
A weCanGenerateANewEntityInstance() 0 6 1
A getTestEntityGenerator() 0 3 1
A getSchemaErrors() 0 8 3
A theSchemaIsValidForThisEntity() 0 11 3
A getEntitySaver() 0 3 1
A dump() 0 3 1
A theReloadedEntityHasNoAssociatedEntities() 0 9 1
A getTestedEntityClassMetaData() 0 3 1
B theEntityGettersReturnValues() 0 39 7
A assertAllAssociationsAreEmpty() 0 28 5

How to fix   Complexity   

Complex Class

Complex classes like AbstractEntityTest 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 AbstractEntityTest, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\DoctrineStaticMeta\Entity\Testing;
4
5
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
6
use Doctrine\ORM\EntityManagerInterface;
7
use Doctrine\ORM\Mapping\ClassMetadata;
8
use Doctrine\ORM\Tools\SchemaValidator;
9
use Doctrine\ORM\Utility\PersisterHelper;
10
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\CodeHelper;
11
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator;
12
use EdmondsCommerce\DoctrineStaticMeta\Config;
13
use EdmondsCommerce\DoctrineStaticMeta\ConfigInterface;
14
use EdmondsCommerce\DoctrineStaticMeta\Entity\DataTransferObjects\DtoFactory;
15
use EdmondsCommerce\DoctrineStaticMeta\Entity\Embeddable\Objects\AbstractEmbeddableObject;
16
use EdmondsCommerce\DoctrineStaticMeta\Entity\Fields\Interfaces\PrimaryKey\IdFieldInterface;
17
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\EntityInterface;
18
use EdmondsCommerce\DoctrineStaticMeta\Entity\Repositories\RepositoryFactory;
19
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\EntitySaverFactory;
20
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\EntitySaverInterface;
21
use EdmondsCommerce\DoctrineStaticMeta\Entity\Testing\EntityGenerator\TestEntityGenerator;
22
use EdmondsCommerce\DoctrineStaticMeta\Entity\Testing\EntityGenerator\TestEntityGeneratorFactory;
23
use EdmondsCommerce\DoctrineStaticMeta\Entity\Testing\Fixtures\AbstractEntityFixtureLoader;
24
use EdmondsCommerce\DoctrineStaticMeta\Entity\Testing\Fixtures\FixturesHelper;
25
use EdmondsCommerce\DoctrineStaticMeta\Exception\ConfigException;
26
use EdmondsCommerce\DoctrineStaticMeta\MappingHelper;
27
use EdmondsCommerce\DoctrineStaticMeta\SimpleEnv;
28
use PHPUnit\Framework\TestCase;
29
use Psr\Container\ContainerInterface;
30
31
/**
32
 * Class AbstractEntityTest
33
 *
34
 * This abstract test is designed to give you a good level of test coverage for your entities without any work required.
35
 *
36
 * You should extend the test with methods that test your specific business logic, your validators and anything else.
37
 *
38
 * You can override the methods, properties and constants as you see fit.
39
 *
40
 * @package EdmondsCommerce\DoctrineStaticMeta\Entity
41
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
42
 * @SuppressWarnings(PHPMD.NumberOfChildren)
43
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
44
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
45
 * @SuppressWarnings(PHPMD.StaticAccess)
46
 */
47
abstract class AbstractEntityTest extends TestCase implements EntityTestInterface
48
{
49
    /**
50
     * @var ContainerInterface
51
     */
52
    protected static $container;
53
    /**
54
     * The fully qualified name of the Entity being tested, as calculated by the test class name
55
     *
56
     * @var string
57
     */
58
    protected static $testedEntityFqn;
59
60
    /**
61
     * @var TestEntityGenerator
62
     */
63
    protected static $testEntityGenerator;
64
65
    /**
66
     * @var array
67
     */
68
    protected static $schemaErrors = [];
69
70
    public static function setUpBeforeClass(): void
71
    {
72
        if (null !== static::$container) {
73
            self::tearDownAfterClass();
74
        }
75
        static::initContainer();
76
        static::$testedEntityFqn     = \substr(static::class, 0, -4);
77
        static::$testEntityGenerator = static::$container->get(TestEntityGeneratorFactory::class)
78
                                                         ->createForEntityFqn(static::$testedEntityFqn);
79
    }
80
81
    public static function tearDownAfterClass()
82
    {
83
        $entityManager = static::$container->get(EntityManagerInterface::class);
84
        $entityManager->close();
85
        $entityManager->getConnection()->close();
86
        self::$container   = null;
87
        static::$container = null;
88
    }
89
90
    public static function initContainer(): void
91
    {
92
        $testConfig        = self::getTestContainerConfig();
93
        static::$container = TestContainerFactory::getContainer($testConfig);
94
    }
95
96
    /**
97
     * @throws ConfigException
98
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
99
     * @SuppressWarnings(PHPMD.Superglobals)
100
     * @SuppressWarnings(PHPMD.StaticAccess)
101
     */
102
    protected static function getTestContainerConfig(): array
103
    {
104
        SimpleEnv::setEnv(Config::getProjectRootDirectory() . '/.env');
105
        $testConfig                                 = $_SERVER;
106
        $testConfig[ConfigInterface::PARAM_DB_NAME] = $_SERVER[ConfigInterface::PARAM_DB_NAME] . '_test';
107
108
        return $testConfig;
109
    }
110
111
    /**
112
     * This test checks that the fixtures can be loaded properly
113
     *
114
     * The test returns the array of fixtures which means you could choose to add a test that `depends` on this test
115
     *
116
     * @test
117
     */
118
    public function theFixtureCanBeLoaded(): array
119
    {
120
        /**
121
         * @var FixturesHelper $fixtureHelper
122
         */
123
        $fixtureHelper = static::$container->get(FixturesHelper::class);
124
        /**
125
         * This can seriously hurt performance, but is needed as a default
126
         */
127
        $fixtureHelper->setLoadFromCache(false);
128
        /**
129
         * @var AbstractEntityFixtureLoader $fixture
130
         */
131
        $fixture = $fixtureHelper->createFixtureInstanceForEntityFqn(static::$testedEntityFqn);
132
        $fixtureHelper->createDb($fixture);
133
        $loaded               = $this->loadAllEntities();
134
        $expectedAmountLoaded = $fixture::BULK_AMOUNT_TO_GENERATE;
135
        $actualAmountLoaded   = count($loaded);
136
        self::assertSame(
137
            $expectedAmountLoaded,
138
            $actualAmountLoaded,
139
            "expected to load $expectedAmountLoaded but actually loaded $actualAmountLoaded"
140
        );
141
142
        return $loaded;
143
    }
144
145
    /**
146
     * Test that we have correctly generated an instance of our test entity
147
     *
148
     * @param array $fixtureEntities
149
     *
150
     * @return EntityInterface
151
     * @SuppressWarnings(PHPMD.StaticAccess)
152
     * @depends theFixtureCanBeLoaded
153
     * @test
154
     */
155
    public function weCanGenerateANewEntityInstance(array $fixtureEntities): EntityInterface
156
    {
157
        $generated = current($fixtureEntities);
158
        self::assertInstanceOf(static::$testedEntityFqn, $generated);
159
160
        return $generated;
161
    }
162
163
    protected function loadAllEntities(): array
164
    {
165
        return static::$container->get(RepositoryFactory::class)
166
                                 ->getRepository(static::$testedEntityFqn)
167
                                 ->findAll();
168
    }
169
170
    /**
171
     * @test
172
     * Use Doctrine's built in schema validation tool to catch issues
173
     */
174
    public function theSchemaIsValidForThisEntity()
175
    {
176
        $errors  = $this->getSchemaErrors();
177
        $message = '';
178
        if (isset($errors[static::$testedEntityFqn])) {
179
            $message = "Failed ORM Validate Schema:\n";
180
            foreach ($errors[static::$testedEntityFqn] as $err) {
181
                $message .= "\n * $err \n";
182
            }
183
        }
184
        self::assertEmpty($message, $message);
185
    }
186
187
    /**
188
     * Use Doctrine's standard schema validation to get errors for the whole schema
189
     *
190
     * We cache this as a class property because the schema only needs validating once as a whole, after that we can
191
     * pull out Entity specific issues as required
192
     *
193
     * @param bool $update
194
     *
195
     * @return array
196
     * @throws \Exception
197
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
198
     */
199
    protected function getSchemaErrors(bool $update = false): array
200
    {
201
        if ([] === static::$schemaErrors || true === $update) {
202
            $validator            = new SchemaValidator($this->getEntityManager());
203
            static::$schemaErrors = $validator->validateMapping();
204
        }
205
206
        return static::$schemaErrors;
207
    }
208
209
    protected function getEntityManager(): EntityManagerInterface
210
    {
211
        return static::$container->get(EntityManagerInterface::class);
212
    }
213
214
    /**
215
     * Loop through Entity fields, call the getter and where possible assert there is a value returned
216
     *
217
     * @param EntityInterface $entity
218
     *
219
     * @return EntityInterface
220
     * @throws \Doctrine\ORM\Query\QueryException
221
     * @throws \ReflectionException
222
     * @SuppressWarnings(PHPMD.StaticAccess)
223
     * @test
224
     * @depends weCanGenerateANewEntityInstance
225
     */
226
    public function theEntityGettersReturnValues(EntityInterface $entity): EntityInterface
227
    {
228
        $meta = $this->getEntityManager()->getClassMetadata(static::$testedEntityFqn);
229
        $dto  = $this->getDtoFactory()->createDtoFromEntity($entity);
230
        foreach ($meta->getFieldNames() as $fieldName) {
231
            if ('id' === $fieldName) {
232
                continue;
233
            }
234
            $type   = PersisterHelper::getTypeOfField($fieldName, $meta, $this->getEntityManager())[0];
235
            $method = $this->getGetterNameForField($fieldName, $type);
236
            if (\ts\stringContains($method, '.')) {
237
                list($getEmbeddableMethod,) = explode('.', $method);
238
                $embeddable = $entity->$getEmbeddableMethod();
239
                self::assertInstanceOf(AbstractEmbeddableObject::class, $embeddable);
240
                continue;
241
            }
242
            $reflectionMethod = new \ReflectionMethod($entity, $method);
243
            if ($reflectionMethod->hasReturnType()) {
244
                $returnType = $reflectionMethod->getReturnType();
245
                $allowsNull = $returnType->allowsNull();
246
                if ($allowsNull) {
247
                    // As we can't assert anything here so simply call
248
                    // the method and allow the type hint to raise any
249
                    // errors.
250
                    $entity->$method();
251
                    continue;
252
                }
253
                self::assertNotNull($dto->$method(), "$fieldName getter returned null");
254
                continue;
255
            }
256
            // If there is no return type then we can't assert anything,
257
            // but again we can just call the getter to check for errors
258
            $dto->$method();
259
        }
260
        if (0 === $this->getCount()) {
261
            self::assertTrue(true);
262
        }
263
264
        return $entity;
265
    }
266
267
    protected function getDtoFactory(): DtoFactory
268
    {
269
        return static::$container->get(DtoFactory::class);
270
    }
271
272
    protected function getGetterNameForField(string $fieldName, string $type): string
273
    {
274
        if ($type === 'boolean') {
275
            return static::$container->get(CodeHelper::class)->getGetterMethodNameForBoolean($fieldName);
276
        }
277
278
        return 'get' . $fieldName;
279
    }
280
281
282
283
    /**
284
     * @param EntityInterface $generated
285
     *
286
     * @return EntityInterface
287
     * @test
288
     * @depends theEntityGettersReturnValues
289
     * @throws \ErrorException
290
     */
291
    public function weCanExtendTheEntityWithUnrequiredAssociationEntities(EntityInterface $generated): EntityInterface
292
    {
293
        if ([] === $this->getTestedEntityClassMetaData()->getAssociationMappings()) {
294
            $this->markTestSkipped('No associations to test');
295
        }
296
        $this->getTestEntityGenerator()->addAssociationEntities($generated);
297
        $this->assertAllAssociationsAreNotEmpty($generated);
298
299
        return $generated;
300
    }
301
302
    protected function getTestedEntityClassMetaData(): ClassMetadata
303
    {
304
        return $this->getEntityManager()->getClassMetadata(static::$testedEntityFqn);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getEntityM...tatic::testedEntityFqn) returns the type Doctrine\Common\Persistence\Mapping\ClassMetadata which includes types incompatible with the type-hinted return Doctrine\ORM\Mapping\ClassMetadata.
Loading history...
305
    }
306
307
    protected function getTestEntityGenerator(): TestEntityGenerator
308
    {
309
        return static::$testEntityGenerator;
310
    }
311
312
    protected function assertAllAssociationsAreNotEmpty(EntityInterface $entity)
313
    {
314
        $meta = $this->getTestedEntityClassMetaData();
315
        foreach ($meta->getAssociationMappings() as $mapping) {
316
            $getter = 'get' . $mapping['fieldName'];
317
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
318
                $collection = $entity->$getter()->toArray();
319
                $this->assertCorrectMappings(static::$testedEntityFqn, $mapping);
320
                self::assertNotEmpty(
321
                    $collection,
322
                    'Failed to load the collection of the associated entity [' . $mapping['fieldName']
323
                    . '] from the generated ' . static::$testedEntityFqn
324
                    . ', make sure you have reciprocal adding of the association'
325
                );
326
327
                continue;
328
            }
329
            $association = $entity->$getter();
330
            self::assertNotEmpty(
331
                $association,
332
                'Failed to load the associated entity: [' . $mapping['fieldName']
333
                . '] from the generated ' . static::$testedEntityFqn
334
            );
335
            self::assertNotEmpty(
336
                $association->getId(),
337
                'Failed to get the ID of the associated entity: [' . $mapping['fieldName']
338
                . '] from the generated ' . static::$testedEntityFqn
339
            );
340
        }
341
    }
342
343
    /**
344
     * Check the mapping of our class and the associated entity to make sure it's configured properly on both sides.
345
     * Very easy to get wrong. This is in addition to the standard Schema Validation
346
     *
347
     * @param string $classFqn
348
     * @param array  $mapping
349
     */
350
    protected function assertCorrectMappings(string $classFqn, array $mapping)
351
    {
352
        $entityManager                        = $this->getEntityManager();
353
        $pass                                 = false;
354
        $associationFqn                       = $mapping['targetEntity'];
355
        $associationMeta                      = $entityManager->getClassMetadata($associationFqn);
356
        $classTraits                          = $entityManager->getClassMetadata($classFqn)
357
                                                              ->getReflectionClass()
358
                                                              ->getTraits();
359
        $unidirectionalTraitShortNamePrefixes = [
360
            'Has' . $associationFqn::getDoctrineStaticMeta()->getSingular() . RelationsGenerator::PREFIX_UNIDIRECTIONAL,
361
            'Has' . $associationFqn::getDoctrineStaticMeta()->getPlural() . RelationsGenerator::PREFIX_UNIDIRECTIONAL,
362
            'Has' . RelationsGenerator::PREFIX_REQUIRED .
363
            $associationFqn::getDoctrineStaticMeta()->getSingular() . RelationsGenerator::PREFIX_UNIDIRECTIONAL,
364
            'Has' . RelationsGenerator::PREFIX_REQUIRED .
365
            $associationFqn::getDoctrineStaticMeta()->getPlural() . RelationsGenerator::PREFIX_UNIDIRECTIONAL,
366
        ];
367
        foreach ($classTraits as $trait) {
368
            foreach ($unidirectionalTraitShortNamePrefixes as $namePrefix) {
369
                if (0 === \stripos($trait->getShortName(), $namePrefix)) {
370
                    return;
371
                }
372
            }
373
        }
374
        foreach ($associationMeta->getAssociationMappings() as $associationMapping) {
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

374
        foreach ($associationMeta->/** @scrutinizer ignore-call */ getAssociationMappings() as $associationMapping) {

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.

Loading history...
375
            if ($classFqn === $associationMapping['targetEntity']) {
376
                $pass = $this->assertCorrectMapping($mapping, $associationMapping, $classFqn);
377
                break;
378
            }
379
        }
380
        self::assertTrue(
381
            $pass,
382
            'Failed finding association mapping to test for ' . "\n" . $mapping['targetEntity']
383
        );
384
    }
385
386
    /**
387
     * @param array  $mapping
388
     * @param array  $associationMapping
389
     * @param string $classFqn
390
     *
391
     * @return bool
392
     */
393
    protected function assertCorrectMapping(array $mapping, array $associationMapping, string $classFqn): bool
394
    {
395
        if (empty($mapping['joinTable'])) {
396
            self::assertArrayNotHasKey(
397
                'joinTable',
398
                $associationMapping,
399
                $classFqn . ' join table is empty,
400
                        but association ' . $mapping['targetEntity'] . ' join table is not empty'
401
            );
402
403
            return true;
404
        }
405
        self::assertNotEmpty(
406
            $associationMapping['joinTable'],
407
            "$classFqn joinTable is set to " . $mapping['joinTable']['name']
408
            . " \n association " . $mapping['targetEntity'] . ' join table is empty'
409
        );
410
        self::assertSame(
411
            $mapping['joinTable']['name'],
412
            $associationMapping['joinTable']['name'],
413
            "join tables not the same: \n * $classFqn = " . $mapping['joinTable']['name']
414
            . " \n * association " . $mapping['targetEntity']
415
            . ' = ' . $associationMapping['joinTable']['name']
416
        );
417
        self::assertArrayHasKey(
418
            'inverseJoinColumns',
419
            $associationMapping['joinTable'],
420
            "join table join columns not the same: \n * $classFqn joinColumn = "
421
            . $mapping['joinTable']['joinColumns'][0]['name']
422
            . " \n * association " . $mapping['targetEntity']
423
            . ' inverseJoinColumn is not set'
424
        );
425
        self::assertSame(
426
            $mapping['joinTable']['joinColumns'][0]['name'],
427
            $associationMapping['joinTable']['inverseJoinColumns'][0]['name'],
428
            "join table join columns not the same: \n * $classFqn joinColumn = "
429
            . $mapping['joinTable']['joinColumns'][0]['name']
430
            . " \n * association " . $mapping['targetEntity']
431
            . ' inverseJoinColumn = ' . $associationMapping['joinTable']['inverseJoinColumns'][0]['name']
432
        );
433
        self::assertSame(
434
            $mapping['joinTable']['inverseJoinColumns'][0]['name'],
435
            $associationMapping['joinTable']['joinColumns'][0]['name'],
436
            "join table join columns  not the same: \n * $classFqn inverseJoinColumn = "
437
            . $mapping['joinTable']['inverseJoinColumns'][0]['name']
438
            . " \n * association " . $mapping['targetEntity'] . ' joinColumn = '
439
            . $associationMapping['joinTable']['joinColumns'][0]['name']
440
        );
441
442
        return true;
443
    }
444
445
    /**
446
     * @param EntityInterface $generated
447
     *
448
     * @return EntityInterface
449
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
450
     * @throws \ReflectionException
451
     * @test
452
     * @depends weCanExtendTheEntityWithUnrequiredAssociationEntities
453
     */
454
    public function theEntityCanBeSavedAndReloadedFromTheDatabase(EntityInterface $generated): EntityInterface
455
    {
456
        $this->getEntitySaver()->save($generated);
457
        $loaded = $this->loadEntity($generated->getId());
458
        self::assertSame((string)$generated->getId(), (string)$loaded->getId());
459
        self::assertInstanceOf(static::$testedEntityFqn, $loaded);
460
461
        return $loaded;
462
    }
463
464
    protected function getEntitySaver(): EntitySaverInterface
465
    {
466
        return static::$container->get(EntitySaverFactory::class)->getSaverForEntityFqn(static::$testedEntityFqn);
467
    }
468
469
    /**
470
     * @param mixed $id
471
     *
472
     * @return EntityInterface|null
473
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
474
     */
475
    protected function loadEntity($id): EntityInterface
476
    {
477
        return static::$container->get(RepositoryFactory::class)
478
                                 ->getRepository(static::$testedEntityFqn)
479
                                 ->get($id);
480
    }
481
482
    /**
483
     * @param EntityInterface $loaded
484
     *
485
     * @return EntityInterface
486
     * @throws ConfigException
487
     * @throws \Doctrine\ORM\Query\QueryException
488
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
489
     * @throws \ErrorException
490
     * @throws \ReflectionException
491
     * @depends theEntityCanBeSavedAndReloadedFromTheDatabase
492
     * @test
493
     */
494
    public function theLoadedEntityCanBeUpdatedAndResaved(EntityInterface $loaded): EntityInterface
495
    {
496
        $this->updateEntityFields($loaded);
497
        $this->assertAllAssociationsAreNotEmpty($loaded);
498
        $this->removeAllAssociations($loaded);
499
        $this->getEntitySaver()->save($loaded);
500
501
        return $loaded;
502
    }
503
504
    /**
505
     * Generate a new entity and then update our Entity with the values from the generated one
506
     *
507
     * @param EntityInterface $entity
508
     *
509
     * @SuppressWarnings(PHPMD.StaticAccess)
510
     */
511
    protected function updateEntityFields(EntityInterface $entity): void
512
    {
513
        $dto = $this->getDtoFactory()->createDtoFromEntity($entity);
514
        $this->getTestEntityGenerator()->fakerUpdateDto($dto);
515
        $entity->update($dto);
516
    }
517
518
    /**
519
     * @param EntityInterface $entity
520
     *
521
     * @throws \ReflectionException
522
     * @SuppressWarnings(PHPMD.StaticAccess)
523
     */
524
    protected function removeAllAssociations(EntityInterface $entity)
525
    {
526
        $required    = $entity::getDoctrineStaticMeta()->getRequiredRelationProperties();
527
        $meta        = $this->getTestedEntityClassMetaData();
528
        $identifiers = array_flip($meta->getIdentifier());
529
        foreach ($meta->getAssociationMappings() as $mapping) {
530
            if (isset($identifiers[$mapping['fieldName']])) {
531
                continue;
532
            }
533
            if (isset($required[$mapping['fieldName']])) {
534
                continue;
535
            }
536
            $remover = 'remove' . MappingHelper::singularize($mapping['fieldName']);
537
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
538
                $getter    = 'get' . $mapping['fieldName'];
539
                $relations = $entity->$getter();
540
                foreach ($relations as $relation) {
541
                    $entity->$remover($relation);
542
                }
543
                continue;
544
            }
545
            $entity->$remover();
546
        }
547
        $this->assertAllAssociationsAreEmpty($entity);
548
    }
549
550
    protected function assertAllAssociationsAreEmpty(EntityInterface $entity)
551
    {
552
        $required    = $entity::getDoctrineStaticMeta()->getRequiredRelationProperties();
553
        $meta        = $this->getTestedEntityClassMetaData();
554
        $identifiers = array_flip($meta->getIdentifier());
555
        foreach ($meta->getAssociationMappings() as $mapping) {
556
            if (isset($identifiers[$mapping['fieldName']])) {
557
                continue;
558
            }
559
            if (isset($required[$mapping['fieldName']])) {
560
                continue;
561
            }
562
563
            $getter = 'get' . $mapping['fieldName'];
564
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
565
                $collection = $entity->$getter()->toArray();
566
                self::assertEmpty(
567
                    $collection,
568
                    'Collection of the associated entity [' . $mapping['fieldName']
569
                    . '] is not empty after calling remove'
570
                );
571
                continue;
572
            }
573
            $association = $entity->$getter();
574
            self::assertEmpty(
575
                $association,
576
                'Failed to remove associated entity: [' . $mapping['fieldName']
577
                . '] from the generated ' . static::$testedEntityFqn
578
            );
579
        }
580
    }
581
582
    /**
583
     * @param EntityInterface $entity
584
     *
585
     * @return EntityInterface
586
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
587
     * @depends theLoadedEntityCanBeUpdatedAndResaved
588
     * @test
589
     */
590
    public function theReloadedEntityHasNoAssociatedEntities(EntityInterface $entity): EntityInterface
591
    {
592
        $reLoaded     = $this->loadEntity($entity->getId());
593
        $entityDump   = $this->dump($entity);
594
        $reLoadedDump = $this->dump($reLoaded);
595
        self::assertEquals($entityDump, $reLoadedDump);
596
        $this->assertAllAssociationsAreEmpty($reLoaded);
597
598
        return $reLoaded;
599
    }
600
601
    protected function dump(EntityInterface $entity): string
602
    {
603
        return (new EntityDebugDumper())->dump($entity, $this->getEntityManager());
604
    }
605
606
    /**
607
     * @depends weCanGenerateANewEntityInstance
608
     *
609
     * @param EntityInterface $entity
610
     *
611
     * @test
612
     */
613
    public function checkAllGettersCanBeReturnedFromDoctrineStaticMeta(EntityInterface $entity)
614
    {
615
        $getters = $entity::getDoctrineStaticMeta()->getGetters();
616
        self::assertNotEmpty($getters);
617
        foreach ($getters as $getter) {
618
            self::assertRegExp('%^(get|is|has).+%', $getter);
619
        }
620
    }
621
622
    /**
623
     * @depends weCanGenerateANewEntityInstance
624
     * @test
625
     *
626
     * @param EntityInterface $entity
627
     */
628
    public function checkAllSettersCanBeReturnedFromDoctrineStaticMeta(EntityInterface $entity)
629
    {
630
        $setters = $entity::getDoctrineStaticMeta()->getSetters();
631
        self::assertNotEmpty($setters);
632
        foreach ($setters as $setter) {
633
            self::assertRegExp('%^(set|add).+%', $setter);
634
        }
635
    }
636
637
    /**
638
     * Loop through entity fields and find unique ones
639
     *
640
     * Then ensure that the unique rule is being enforced as expected
641
     *
642
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
643
     * @throws \ErrorException
644
     * @throws \ReflectionException
645
     * @test
646
     * @depends theReloadedEntityHasNoAssociatedEntities
647
     */
648
    public function checkThatWeCanNotSaveEntitiesWithDuplicateUniqueFieldValues(): void
649
    {
650
        $meta         = $this->getTestedEntityClassMetaData();
651
        $uniqueFields = [];
652
        foreach ($meta->getFieldNames() as $fieldName) {
653
            if (IdFieldInterface::PROP_ID === $fieldName) {
654
                continue;
655
            }
656
            if (true === $this->isUniqueField($fieldName)) {
657
                $uniqueFields[] = $fieldName;
658
            }
659
        }
660
        if ([] === $uniqueFields) {
661
            self::markTestSkipped('No unique fields to check');
662
663
            return;
664
        }
665
        foreach ($uniqueFields as $fieldName) {
666
            $primary      = $this->getTestEntityGenerator()->generateEntity();
667
            $secondary    = $this->getTestEntityGenerator()->generateEntity();
668
            $secondaryDto = $this->getDtoFactory()->createDtoFromEntity($secondary);
669
            $getter       = 'get' . $fieldName;
670
            $setter       = 'set' . $fieldName;
671
            $primaryValue = $primary->$getter();
672
            $secondaryDto->$setter($primaryValue);
673
            $secondary->update($secondaryDto);
674
            $saver = $this->getEntitySaver();
675
            $this->expectException(UniqueConstraintViolationException::class);
676
            $saver->saveAll([$primary, $secondary]);
677
        }
678
    }
679
680
    protected function isUniqueField(string $fieldName): bool
681
    {
682
        $fieldMapping = $this->getTestedEntityClassMetaData()->getFieldMapping($fieldName);
683
684
        return array_key_exists('unique', $fieldMapping) && true === $fieldMapping['unique'];
685
    }
686
}
687