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 (#126)
by joseph
28:55
created

AbstractEntityTest::getSchemaErrors()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 9
rs 10
c 0
b 0
f 0
ccs 0
cts 6
cp 0
cc 3
nc 2
nop 1
crap 12
1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\DoctrineStaticMeta\Entity\Testing;
4
5
use Doctrine\Common\Cache\ArrayCache;
6
use Doctrine\Common\Inflector\Inflector;
7
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
8
use Doctrine\ORM\EntityManagerInterface;
9
use Doctrine\ORM\Mapping\ClassMetadata;
10
use Doctrine\ORM\Tools\SchemaValidator;
11
use Doctrine\ORM\Utility\PersisterHelper;
12
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\CodeHelper;
13
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator;
14
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\NamespaceHelper;
15
use EdmondsCommerce\DoctrineStaticMeta\Config;
16
use EdmondsCommerce\DoctrineStaticMeta\ConfigInterface;
17
use EdmondsCommerce\DoctrineStaticMeta\Entity\Embeddable\Objects\AbstractEmbeddableObject;
18
use EdmondsCommerce\DoctrineStaticMeta\Entity\Factory\EntityFactory;
19
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\EntityInterface;
20
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\EntitySaver;
21
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\EntitySaverFactory;
22
use EdmondsCommerce\DoctrineStaticMeta\Entity\Testing\EntityGenerator\TestEntityGenerator;
23
use EdmondsCommerce\DoctrineStaticMeta\Entity\Validation\EntityValidatorFactory;
24
use EdmondsCommerce\DoctrineStaticMeta\EntityManager\EntityManagerFactory;
25
use EdmondsCommerce\DoctrineStaticMeta\Exception\ConfigException;
26
use EdmondsCommerce\DoctrineStaticMeta\SimpleEnv;
27
use PHPUnit\Framework\TestCase;
28
use Symfony\Component\Validator\Mapping\Cache\DoctrineCache;
29
30
/**
31
 * Class AbstractEntityTest
32
 *
33
 * This abstract test is designed to give you a good level of test coverage for your entities without any work required.
34
 *
35
 * You should extend the test with methods that test your specific business logic, your validators and anything else.
36
 *
37
 * You can override the methods, properties and constants as you see fit.
38
 *
39
 * @package EdmondsCommerce\DoctrineStaticMeta\Entity
40
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
41
 * @SuppressWarnings(PHPMD.NumberOfChildren)
42
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
43
 */
44
abstract class AbstractEntityTest extends TestCase implements EntityTestInterface
45
{
46
    /**
47
     * The fully qualified name of the Entity being tested, as calculated by the test class name
48
     *
49
     * @var string
50
     */
51
    protected $testedEntityFqn;
52
53
    /**
54
     * Reflection of the tested entity
55
     *
56
     * @var \ts\Reflection\ReflectionClass
57
     */
58
    protected $testedEntityReflectionClass;
59
60
61
    /**
62
     * @var EntityValidatorFactory
63
     */
64
    protected $entityValidatorFactory;
65
66
    /**
67
     * @var EntityManagerInterface
68
     */
69
    protected $entityManager;
70
71
    /**
72
     * @var array
73
     */
74
    protected $schemaErrors = [];
75
76
77
    /**
78
     * @var TestEntityGenerator
79
     */
80
    protected $testEntityGenerator;
81
82
    /**
83
     * @var EntitySaverFactory
84
     */
85
    protected $entitySaverFactory;
86
87
    /**
88
     * @var CodeHelper
89
     */
90
    protected $codeHelper;
91
92
    /**
93
     * @var EntityDebugDumper
94
     */
95
    protected $dumper;
96
97
    /**
98
     * Use Doctrine's built in schema validation tool to catch issues
99
     */
100
    public function testValidateSchema()
101
    {
102
        $errors  = $this->getSchemaErrors();
103
        $class   = $this->getTestedEntityFqn();
104
        $message = '';
105
        if (isset($errors[$class])) {
106
            $message = "Failed ORM Validate Schema:\n";
107
            foreach ($errors[$class] as $err) {
108
                $message .= "\n * $err \n";
109
            }
110
        }
111
        self::assertEmpty($message, $message);
112
    }
113
114
    /**
115
     * Use Doctrine's standard schema validation to get errors for the whole schema
116
     *
117
     * @param bool $update
118
     *
119
     * @return array
120
     * @throws \Exception
121
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
122
     */
123
    protected function getSchemaErrors(bool $update = false): array
124
    {
125
        if (empty($this->schemaErrors) || true === $update) {
126
            $entityManager      = $this->getEntityManager();
127
            $validator          = new SchemaValidator($entityManager);
128
            $this->schemaErrors = $validator->validateMapping();
129
        }
130
131
        return $this->schemaErrors;
132
    }
133
134
    public function testConstructor(): EntityInterface
135
    {
136
        $class  = $this->getTestedEntityFqn();
137
        $entity = new $class($this->entityValidatorFactory);
138
        self::assertInstanceOf($class, $entity);
139
140
        return $entity;
141
    }
142
143
    /**
144
     * @param EntityInterface $entity
145
     *
146
     * @throws ConfigException
147
     * @throws \Doctrine\ORM\Query\QueryException
148
     * @throws \ReflectionException
149
     * @depends testConstructor
150
     */
151
    public function testGetDefaults(EntityInterface $entity)
152
    {
153
        $this->callEntityGettersAndAssertNotNull($entity);
154
    }
155
156
    /**
157
     * Loop through Entity fields, call the getter and where possible assert there is a value returned
158
     *
159
     * @param EntityInterface $entity
160
     *
161
     * @throws ConfigException
162
     * @throws \Doctrine\ORM\Query\QueryException
163
     * @throws \ReflectionException
164
     * @SuppressWarnings(PHPMD.StaticAccess)
165
     */
166
    protected function callEntityGettersAndAssertNotNull(EntityInterface $entity): void
167
    {
168
        $class         = $this->getTestedEntityFqn();
169
        $entityManager = $this->getEntityManager();
170
        $meta          = $entityManager->getClassMetadata($class);
171
        foreach ($meta->getFieldNames() as $fieldName) {
172
            $type   = PersisterHelper::getTypeOfField($fieldName, $meta, $entityManager)[0];
173
            $method = $this->getGetterNameForField($fieldName, $type);
174
            if (\ts\stringContains($method, '.')) {
175
                list($getEmbeddableMethod,) = explode('.', $method);
176
                $embeddable = $entity->$getEmbeddableMethod();
177
                self::assertInstanceOf(AbstractEmbeddableObject::class, $embeddable);
178
                continue;
179
            }
180
            $reflectionMethod = new \ReflectionMethod($entity, $method);
181
            if ($reflectionMethod->hasReturnType()) {
182
                $returnType = $reflectionMethod->getReturnType();
183
                $allowsNull = $returnType->allowsNull();
184
                if ($allowsNull) {
185
                    // As we can't assert anything here so simply call
186
                    // the method and allow the type hint to raise any
187
                    // errors.
188
                    $entity->$method();
189
                    continue;
190
                }
191
                self::assertNotNull($entity->$method(), "$fieldName getter returned null");
192
                continue;
193
            }
194
            // If there is no return type then we can't assert anything,
195
            // but again we can just call the getter to check for errors
196
            $entity->$method();
197
        }
198
        if (0 === $this->getCount()) {
199
            self::markTestSkipped('No assertable getters in this Entity');
200
        }
201
    }
202
203
    protected function getGetterNameForField(string $fieldName, string $type): string
204
    {
205
        if ($type === 'boolean') {
206
            return $this->codeHelper->getGetterMethodNameForBoolean($fieldName);
207
        }
208
209
        return 'get' . $fieldName;
210
    }
211
212
    /**
213
     * Test that we have correctly generated an instance of our test entity
214
     *
215
     * @throws ConfigException
216
     * @throws \Doctrine\ORM\Query\QueryException
217
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
218
     * @throws \Exception
219
     * @throws \ReflectionException
220
     * @SuppressWarnings(PHPMD.StaticAccess)
221
     */
222
    public function testGeneratedCreate(): EntityInterface
223
    {
224
        $entityManager = $this->getEntityManager();
225
        $class         = $this->getTestedEntityFqn();
226
        $generated     = $this->testEntityGenerator->generateEntity($entityManager, $class);
227
        self::assertInstanceOf($class, $generated);
228
        $this->testEntityGenerator->addAssociationEntities($entityManager, $generated);
229
        $this->validateEntity($generated);
230
        $this->callEntityGettersAndAssertNotNull($generated);
231
        $this->entitySaverFactory->getSaverForEntity($generated)->save($generated);
232
233
        return $generated;
234
    }
235
236
    protected function validateEntity(EntityInterface $entity): void
237
    {
238
        $entity->validate();
239
    }
240
241
    /**
242
     * Test that we can load the entity and then get and set
243
     *
244
     * @param EntityInterface $entity
245
     *
246
     * @return EntityInterface|null
247
     * @throws ConfigException
248
     * @throws \Doctrine\ORM\Mapping\MappingException
249
     * @throws \Doctrine\ORM\Query\QueryException
250
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
251
     * @throws \ReflectionException
252
     * @depends testGeneratedCreate
253
     */
254
    public function testLoadedEntity(EntityInterface $entity): EntityInterface
255
    {
256
        $class         = $this->getTestedEntityFqn();
257
        $entityManager = $this->getEntityManager();
258
        $loaded        = $this->loadEntity($class, $entity->getId(), $entityManager);
259
        self::assertSame($entity->getId(), $loaded->getId());
260
        self::assertInstanceOf($class, $loaded);
261
        $this->updateEntityFields($loaded);
262
        $this->assertAllAssociationsAreNotEmpty($loaded);
263
        $this->validateEntity($loaded);
264
        $this->removeAllAssociations($loaded);
265
        $this->assertAllAssociationsAreEmpty($loaded);
266
        $this->entitySaverFactory->getSaverForEntity($loaded)->save($loaded);
267
268
        return $loaded;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $loaded could return the type null which is incompatible with the type-hinted return EdmondsCommerce\Doctrine...erfaces\EntityInterface. Consider adding an additional type-check to rule them out.
Loading history...
269
    }
270
271
    /**
272
     * @param string                 $class
273
     * @param int|string             $id
274
     * @param EntityManagerInterface $entityManager
275
     *
276
     * @return EntityInterface|null
277
     */
278
    protected function loadEntity(string $class, $id, EntityManagerInterface $entityManager): ?EntityInterface
279
    {
280
        return $entityManager->getRepository($class)->find($id);
281
    }
282
283
    /**
284
     * Generate a new entity and then update our Entity with the values from the generated one
285
     *
286
     * @param EntityInterface $entity
287
     *
288
     * @throws ConfigException
289
     * @throws \Doctrine\ORM\Mapping\MappingException
290
     * @throws \Doctrine\ORM\Query\QueryException
291
     * @throws \ReflectionException
292
     * @SuppressWarnings(PHPMD.StaticAccess)
293
     */
294
    protected function updateEntityFields(EntityInterface $entity): void
295
    {
296
        $class         = $this->getTestedEntityFqn();
297
        $entityManager = $this->getEntityManager();
298
        $meta          = $entityManager->getClassMetadata($class);
299
        $entityManager = $this->getEntityManager();
300
        $class         = $this->getTestedEntityFqn();
301
        $generated     = $this->testEntityGenerator->generateEntity($entityManager, $class, 10);
302
        $identifiers   = \array_flip($meta->getIdentifier());
303
        foreach ($meta->getFieldNames() as $fieldName) {
304
            if (isset($identifiers[$fieldName])) {
305
                continue;
306
            }
307
            if (true === $this->isUniqueField($meta, $fieldName)) {
308
                continue;
309
            }
310
            $setter = 'set' . $fieldName;
311
            if (!\method_exists($entity, $setter)) {
312
                continue;
313
            }
314
            $type   = PersisterHelper::getTypeOfField($fieldName, $meta, $entityManager)[0];
315
            $getter = $this->getGetterNameForField($fieldName, $type);
316
            if (\ts\stringContains($getter, '.')) {
317
                list($getEmbeddableMethod, $fieldInEmbeddable) = explode('.', $getter);
318
                $getterInEmbeddable  = 'get' . $fieldInEmbeddable;
319
                $setterInEmbeddable  = 'set' . $fieldInEmbeddable;
320
                $generatedEmbeddable = $generated->$getEmbeddableMethod();
321
                $embeddable          = $entity->$getEmbeddableMethod();
322
                if (\method_exists($embeddable, $setterInEmbeddable)
323
                    && \method_exists($embeddable, $getterInEmbeddable)
324
                ) {
325
                    $embeddable->$setterInEmbeddable($generatedEmbeddable->$getterInEmbeddable());
326
                }
327
                continue;
328
            }
329
            $entity->$setter($generated->$getter());
330
        }
331
    }
332
333
    protected function isUniqueField(ClassMetadata $meta, string $fieldName): bool
334
    {
335
        $fieldMapping = $meta->getFieldMapping($fieldName);
336
337
        return array_key_exists('unique', $fieldMapping) && true === $fieldMapping['unique'];
338
    }
339
340
    protected function assertAllAssociationsAreNotEmpty(EntityInterface $entity)
341
    {
342
        $entityManager = $this->getEntityManager();
343
        $class         = $this->getTestedEntityFqn();
344
        $meta          = $entityManager->getClassMetadata($class);
345
        foreach ($meta->getAssociationMappings() as $mapping) {
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

345
        foreach ($meta->/** @scrutinizer ignore-call */ getAssociationMappings() as $mapping) {

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...
346
            $getter = 'get' . $mapping['fieldName'];
347
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
348
                $collection = $entity->$getter()->toArray();
349
                $this->assertCorrectMappings($class, $mapping, $entityManager);
350
                self::assertNotEmpty(
351
                    $collection,
352
                    'Failed to load the collection of the associated entity [' . $mapping['fieldName']
353
                    . '] from the generated ' . $class
354
                    . ', make sure you have reciprocal adding of the association'
355
                );
356
357
                continue;
358
            }
359
            $association = $entity->$getter();
360
            self::assertNotEmpty(
361
                $association,
362
                'Failed to load the associated entity: [' . $mapping['fieldName']
363
                . '] from the generated ' . $class
364
            );
365
            self::assertNotEmpty(
366
                $association->getId(),
367
                'Failed to get the ID of the associated entity: [' . $mapping['fieldName']
368
                . '] from the generated ' . $class
369
            );
370
        }
371
    }
372
373
    /**
374
     * Check the mapping of our class and the associated entity to make sure it's configured properly on both sides.
375
     * Very easy to get wrong. This is in addition to the standard Schema Validation
376
     *
377
     * @param string                 $classFqn
378
     * @param array                  $mapping
379
     * @param EntityManagerInterface $entityManager
380
     */
381
    protected function assertCorrectMappings(string $classFqn, array $mapping, EntityManagerInterface $entityManager)
382
    {
383
        $pass                                 = false;
384
        $associationFqn                       = $mapping['targetEntity'];
385
        $associationMeta                      = $entityManager->getClassMetadata($associationFqn);
386
        $classTraits                          = $entityManager->getClassMetadata($classFqn)
387
                                                              ->getReflectionClass()
388
                                                              ->getTraits();
389
        $unidirectionalTraitShortNamePrefixes = [
390
            'Has' . $associationFqn::getDoctrineStaticMeta()->getSingular() . RelationsGenerator::PREFIX_UNIDIRECTIONAL,
391
            'Has' . $associationFqn::getDoctrineStaticMeta()->getPlural() . RelationsGenerator::PREFIX_UNIDIRECTIONAL,
392
        ];
393
        foreach ($classTraits as $trait) {
394
            foreach ($unidirectionalTraitShortNamePrefixes as $namePrefix) {
395
                if (0 === \stripos($trait->getShortName(), $namePrefix)) {
396
                    return;
397
                }
398
            }
399
        }
400
        foreach ($associationMeta->getAssociationMappings() as $associationMapping) {
401
            if ($classFqn === $associationMapping['targetEntity']) {
402
                $pass = self::assertCorrectMapping($mapping, $associationMapping, $classFqn);
0 ignored issues
show
Bug Best Practice introduced by
The method EdmondsCommerce\Doctrine...:assertCorrectMapping() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

402
                /** @scrutinizer ignore-call */ 
403
                $pass = self::assertCorrectMapping($mapping, $associationMapping, $classFqn);
Loading history...
403
                break;
404
            }
405
        }
406
        self::assertTrue(
407
            $pass,
408
            'Failed finding association mapping to test for ' . "\n" . $mapping['targetEntity']
409
        );
410
    }
411
412
    /**
413
     * @param array  $mapping
414
     * @param array  $associationMapping
415
     * @param string $classFqn
416
     *
417
     * @return bool
418
     */
419
    protected function assertCorrectMapping(array $mapping, array $associationMapping, string $classFqn): bool
420
    {
421
        if (empty($mapping['joinTable'])) {
422
            self::assertArrayNotHasKey(
423
                'joinTable',
424
                $associationMapping,
425
                $classFqn . ' join table is empty,
426
                        but association ' . $mapping['targetEntity'] . ' join table is not empty'
427
            );
428
429
            return true;
430
        }
431
        self::assertNotEmpty(
432
            $associationMapping['joinTable'],
433
            "$classFqn joinTable is set to " . $mapping['joinTable']['name']
434
            . " \n association " . $mapping['targetEntity'] . ' join table is empty'
435
        );
436
        self::assertSame(
437
            $mapping['joinTable']['name'],
438
            $associationMapping['joinTable']['name'],
439
            "join tables not the same: \n * $classFqn = " . $mapping['joinTable']['name']
440
            . " \n * association " . $mapping['targetEntity']
441
            . ' = ' . $associationMapping['joinTable']['name']
442
        );
443
        self::assertArrayHasKey(
444
            'inverseJoinColumns',
445
            $associationMapping['joinTable'],
446
            "join table join columns not the same: \n * $classFqn joinColumn = "
447
            . $mapping['joinTable']['joinColumns'][0]['name']
448
            . " \n * association " . $mapping['targetEntity']
449
            . ' inverseJoinColumn is not set'
450
        );
451
        self::assertSame(
452
            $mapping['joinTable']['joinColumns'][0]['name'],
453
            $associationMapping['joinTable']['inverseJoinColumns'][0]['name'],
454
            "join table join columns not the same: \n * $classFqn joinColumn = "
455
            . $mapping['joinTable']['joinColumns'][0]['name']
456
            . " \n * association " . $mapping['targetEntity']
457
            . ' inverseJoinColumn = ' . $associationMapping['joinTable']['inverseJoinColumns'][0]['name']
458
        );
459
        self::assertSame(
460
            $mapping['joinTable']['inverseJoinColumns'][0]['name'],
461
            $associationMapping['joinTable']['joinColumns'][0]['name'],
462
            "join table join columns  not the same: \n * $classFqn inverseJoinColumn = "
463
            . $mapping['joinTable']['inverseJoinColumns'][0]['name']
464
            . " \n * association " . $mapping['targetEntity'] . ' joinColumn = '
465
            . $associationMapping['joinTable']['joinColumns'][0]['name']
466
        );
467
468
        return true;
469
    }
470
471
    /**
472
     * @param EntityInterface $entity
473
     *
474
     * @throws ConfigException
475
     * @SuppressWarnings(PHPMD.StaticAccess)
476
     */
477
    protected function removeAllAssociations(EntityInterface $entity)
478
    {
479
        $entityManager = $this->getEntityManager();
480
        $class         = $this->getTestedEntityFqn();
481
        $meta          = $entityManager->getClassMetadata($class);
482
        $identifiers   = array_flip($meta->getIdentifier());
483
        foreach ($meta->getAssociationMappings() as $mapping) {
484
            if (isset($identifiers[$mapping['fieldName']])) {
485
                continue;
486
            }
487
            $remover = 'remove' . Inflector::singularize($mapping['fieldName']);
488
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
489
                $getter    = 'get' . $mapping['fieldName'];
490
                $relations = $entity->$getter();
491
                foreach ($relations as $relation) {
492
                    $entity->$remover($relation);
493
                }
494
                continue;
495
            }
496
            $entity->$remover();
497
        }
498
        $this->assertAllAssociationsAreEmpty($entity);
499
    }
500
501
    protected function assertAllAssociationsAreEmpty(EntityInterface $entity)
502
    {
503
        $entityManager = $this->getEntityManager();
504
        $class         = $this->getTestedEntityFqn();
505
        $meta          = $entityManager->getClassMetadata($class);
506
        $identifiers   = array_flip($meta->getIdentifier());
507
        foreach ($meta->getAssociationMappings() as $mapping) {
508
            if (isset($identifiers[$mapping['fieldName']])) {
509
                continue;
510
            }
511
512
            $getter = 'get' . $mapping['fieldName'];
513
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
514
                $collection = $entity->$getter()->toArray();
515
                self::assertEmpty(
516
                    $collection,
517
                    'Collection of the associated entity [' . $mapping['fieldName']
518
                    . '] is not empty after calling remove'
519
                );
520
                continue;
521
            }
522
            $association = $entity->$getter();
523
            self::assertEmpty(
524
                $association,
525
                'Failed to remove associated entity: [' . $mapping['fieldName']
526
                . '] from the generated ' . $class
527
            );
528
        }
529
    }
530
531
    /**
532
     * @param EntityInterface $entity
533
     *
534
     * @throws ConfigException
535
     * @depends testLoadedEntity
536
     */
537
    public function testReloadedEntityHasNoAssociations(EntityInterface $entity): void
538
    {
539
        $class         = $this->getTestedEntityFqn();
540
        $entityManager = $this->getEntityManager();
541
        $reLoaded      = $this->loadEntity($class, $entity->getId(), $entityManager);
542
        $entityDump    = $this->dump($entity);
543
        $reLoadedDump  = $this->dump($reLoaded);
544
        self::assertEquals($entityDump, $reLoadedDump);
545
        $this->assertAllAssociationsAreEmpty($reLoaded);
546
    }
547
548
    protected function dump(EntityInterface $entity): string
549
    {
550
        return $this->dumper->dump($entity, $this->getEntityManager());
551
    }
552
553
    /**
554
     * @depends testConstructor
555
     *
556
     * @param EntityInterface $entity
557
     */
558
    public function testGetGetters(EntityInterface $entity)
559
    {
560
        $getters = $entity::getDoctrineStaticMeta()->getGetters();
561
        self::assertNotEmpty($getters);
562
        foreach ($getters as $getter) {
563
            self::assertRegExp('%^(get|is|has).+%', $getter);
564
        }
565
    }
566
567
    /**
568
     * @depends testConstructor
569
     *
570
     * @param EntityInterface $entity
571
     */
572
    public function testSetSetters(EntityInterface $entity)
573
    {
574
        $setters = $entity::getDoctrineStaticMeta()->getSetters();
575
        self::assertNotEmpty($setters);
576
        foreach ($setters as $setter) {
577
            self::assertRegExp('%^(set|add).+%', $setter);
578
        }
579
    }
580
581
    /**
582
     * Loop through entity fields and find unique ones
583
     *
584
     * Then ensure that the unique rule is being enforced as expected
585
     *
586
     * @throws ConfigException
587
     * @throws \Doctrine\ORM\Mapping\MappingException
588
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
589
     * @throws \ReflectionException
590
     * @throws \Doctrine\ORM\Mapping\MappingException
591
     */
592
    public function testUniqueFieldsMustBeUnique(): void
593
    {
594
        $class         = $this->getTestedEntityFqn();
595
        $entityManager = $this->getEntityManager();
596
        $meta          = $entityManager->getClassMetadata($class);
597
        $uniqueFields  = [];
598
        foreach ($meta->getFieldNames() as $fieldName) {
599
            if (true === $this->isUniqueField($meta, $fieldName)) {
600
                $uniqueFields[] = $fieldName;
601
            }
602
        }
603
        if ([] === $uniqueFields) {
604
            self::markTestSkipped('No unique fields to check');
605
606
            return;
607
        }
608
        foreach ($uniqueFields as $fieldName) {
609
            $primary      = $this->testEntityGenerator->generateEntity($entityManager, $class);
610
            $secondary    = $this->testEntityGenerator->generateEntity($entityManager, $class);
611
            $getter       = 'get' . $fieldName;
612
            $setter       = 'set' . $fieldName;
613
            $primaryValue = $primary->$getter();
614
            $secondary->$setter($primaryValue);
615
            $saver = $this->entitySaverFactory->getSaverForEntity($primary);
616
            $this->expectException(UniqueConstraintViolationException::class);
617
            $saver->saveAll([$primary, $secondary]);
618
        }
619
    }
620
621
    /**
622
     * @throws ConfigException
623
     * @throws \Exception
624
     * @SuppressWarnings(PHPMD.StaticAccess)
625
     */
626
    protected function setup()
627
    {
628
        $this->getEntityManager(true);
629
        $this->entityValidatorFactory = new EntityValidatorFactory(new DoctrineCache(new ArrayCache()));
630
        $this->entitySaverFactory     = new EntitySaverFactory(
631
            $this->entityManager,
632
            new EntitySaver($this->entityManager),
633
            new NamespaceHelper()
634
        );
635
        $this->testEntityGenerator    = new TestEntityGenerator(
636
            static::FAKER_DATA_PROVIDERS,
637
            $this->getTestedEntityReflectionClass(),
638
            $this->entitySaverFactory,
639
            $this->entityValidatorFactory,
640
            static::SEED
641
        );
642
        $this->codeHelper             = new CodeHelper(new NamespaceHelper());
643
        $this->dumper                 = new EntityDebugDumper();
644
    }
645
646
    /**
647
     * If a global function dsmGetEntityManagerFactory is defined, we use this
648
     *
649
     * Otherwise, we use the standard DevEntityManagerFactory,
650
     * we define a DB name which is the main DB from env but with `_test` suffixed
651
     *
652
     * @param bool $new
653
     *
654
     * @return EntityManagerInterface
655
     * @throws ConfigException
656
     * @throws \Exception
657
     * @SuppressWarnings(PHPMD)
658
     */
659
    protected function getEntityManager(bool $new = false): EntityManagerInterface
660
    {
661
        if (null === $this->entityManager || true === $new) {
662
            if (\function_exists(self::GET_ENTITY_MANAGER_FUNCTION_NAME)) {
663
                $this->entityManager = \call_user_func(self::GET_ENTITY_MANAGER_FUNCTION_NAME);
664
            } else {
665
                SimpleEnv::setEnv(Config::getProjectRootDirectory() . '/.env');
666
                $testConfig                                 = $_SERVER;
667
                $testConfig[ConfigInterface::PARAM_DB_NAME] = $_SERVER[ConfigInterface::PARAM_DB_NAME] . '_test';
668
                $config                                     = new Config($testConfig);
669
                $this->entityManager                        =
670
                    (new EntityManagerFactory(
671
                        new ArrayCache(),
672
                        new EntityFactory(
673
                            new EntityValidatorFactory(
674
                                new DoctrineCache(
675
                                    new ArrayCache()
676
                                )
677
                            ),
678
                            new NamespaceHelper()
679
                        )
680
                    ))->getEntityManager($config);
681
            }
682
        }
683
684
        return $this->entityManager;
685
    }
686
687
    /**
688
     * Get a \ReflectionClass for the currently tested Entity
689
     *
690
     * @return \ts\Reflection\ReflectionClass
691
     * @throws \ReflectionException
692
     */
693
    protected function getTestedEntityReflectionClass(): \ts\Reflection\ReflectionClass
694
    {
695
        if (null === $this->testedEntityReflectionClass) {
696
            $this->testedEntityReflectionClass = new \ts\Reflection\ReflectionClass(
697
                $this->getTestedEntityFqn()
698
            );
699
        }
700
701
        return $this->testedEntityReflectionClass;
702
    }
703
704
    /**
705
     * Get the fully qualified name of the Entity we are testing,
706
     * assumes EntityNameTest as the entity class short name
707
     *
708
     * @return string
709
     */
710
    protected function getTestedEntityFqn(): string
711
    {
712
        if (null === $this->testedEntityFqn) {
713
            $this->testedEntityFqn = \substr(static::class, 0, -4);
714
        }
715
716
        return $this->testedEntityFqn;
717
    }
718
719
    /**
720
     * @throws ConfigException
721
     */
722
    protected function tearDown()
723
    {
724
        $entityManager = $this->getEntityManager(false);
725
        $connection    = $entityManager->getConnection();
726
727
        $entityManager->close();
728
        $connection->close();
729
    }
730
}
731