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
Push — master ( 72cdd3...9b538e )
by joseph
20:58 queued 16:03
created

AbstractEntityTest::tearDown()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 7
ccs 0
cts 5
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
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
     * @throws ConfigException
99
     * @throws \Exception
100
     * @SuppressWarnings(PHPMD.StaticAccess)
101
     */
102
    protected function setup()
103
    {
104
        $this->getEntityManager(true);
105
        $this->entityValidatorFactory = new EntityValidatorFactory(new DoctrineCache(new ArrayCache()));
106
        $this->entitySaverFactory     = new EntitySaverFactory(
107
            $this->entityManager,
108
            new EntitySaver($this->entityManager),
109
            new NamespaceHelper()
110
        );
111
        $this->testEntityGenerator    = new TestEntityGenerator(
112
            static::FAKER_DATA_PROVIDERS,
113
            $this->getTestedEntityReflectionClass(),
114
            $this->entitySaverFactory,
115
            $this->entityValidatorFactory,
116
            static::SEED
117
        );
118
        $this->codeHelper             = new CodeHelper(new NamespaceHelper());
119
        $this->dumper                 = new EntityDebugDumper();
120
    }
121
122
    /**
123
     * @throws ConfigException
124
     */
125
    protected function tearDown()
126
    {
127
        $entityManager = $this->getEntityManager(false);
128
        $connection    = $entityManager->getConnection();
129
130
        $entityManager->close();
131
        $connection->close();
132
    }
133
134
    /**
135
     * Use Doctrine's standard schema validation to get errors for the whole schema
136
     *
137
     * @param bool $update
138
     *
139
     * @return array
140
     * @throws \Exception
141
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
142
     */
143
    protected function getSchemaErrors(bool $update = false): array
144
    {
145
        if (empty($this->schemaErrors) || true === $update) {
146
            $entityManager      = $this->getEntityManager();
147
            $validator          = new SchemaValidator($entityManager);
148
            $this->schemaErrors = $validator->validateMapping();
149
        }
150
151
        return $this->schemaErrors;
152
    }
153
154
    /**
155
     * If a global function dsmGetEntityManagerFactory is defined, we use this
156
     *
157
     * Otherwise, we use the standard DevEntityManagerFactory,
158
     * we define a DB name which is the main DB from env but with `_test` suffixed
159
     *
160
     * @param bool $new
161
     *
162
     * @return EntityManagerInterface
163
     * @throws ConfigException
164
     * @throws \Exception
165
     * @SuppressWarnings(PHPMD)
166
     */
167
    protected function getEntityManager(bool $new = false): EntityManagerInterface
168
    {
169
        if (null === $this->entityManager || true === $new) {
170
            if (\function_exists(self::GET_ENTITY_MANAGER_FUNCTION_NAME)) {
171
                $this->entityManager = \call_user_func(self::GET_ENTITY_MANAGER_FUNCTION_NAME);
172
            } else {
173
                SimpleEnv::setEnv(Config::getProjectRootDirectory() . '/.env');
174
                $testConfig                                 = $_SERVER;
175
                $testConfig[ConfigInterface::PARAM_DB_NAME] = $_SERVER[ConfigInterface::PARAM_DB_NAME] . '_test';
176
                $config                                     = new Config($testConfig);
177
                $this->entityManager                        =
178
                    (new EntityManagerFactory(
179
                        new ArrayCache(),
180
                        new EntityFactory(
181
                            new EntityValidatorFactory(
182
                                new DoctrineCache(
183
                                    new ArrayCache()
184
                                )
185
                            )
186
                        )
187
                    ))->getEntityManager($config);
188
            }
189
        }
190
191
        return $this->entityManager;
192
    }
193
194
195
    /**
196
     * Use Doctrine's built in schema validation tool to catch issues
197
     */
198
    public function testValidateSchema()
199
    {
200
        $errors  = $this->getSchemaErrors();
201
        $class   = $this->getTestedEntityFqn();
202
        $message = '';
203
        if (isset($errors[$class])) {
204
            $message = "Failed ORM Validate Schema:\n";
205
            foreach ($errors[$class] as $err) {
206
                $message .= "\n * $err \n";
207
            }
208
        }
209
        self::assertEmpty($message, $message);
210
    }
211
212
213
    /**
214
     * @param string                 $class
215
     * @param int|string             $id
216
     * @param EntityManagerInterface $entityManager
217
     *
218
     * @return EntityInterface|null
219
     */
220
    protected function loadEntity(string $class, $id, EntityManagerInterface $entityManager): ?EntityInterface
221
    {
222
        return $entityManager->getRepository($class)->find($id);
223
    }
224
225
    public function testConstructor(): EntityInterface
226
    {
227
        $class  = $this->getTestedEntityFqn();
228
        $entity = new $class($this->entityValidatorFactory);
229
        self::assertInstanceOf($class, $entity);
230
231
        return $entity;
232
    }
233
234
    /**
235
     * @param EntityInterface $entity
236
     *
237
     * @throws ConfigException
238
     * @throws \Doctrine\ORM\Query\QueryException
239
     * @throws \ReflectionException
240
     * @depends testConstructor
241
     */
242
    public function testGetDefaults(EntityInterface $entity)
243
    {
244
        $this->callEntityGettersAndAssertNotNull($entity);
245
    }
246
247
    /**
248
     * Loop through Entity fields, call the getter and where possible assert there is a value returned
249
     *
250
     * @param EntityInterface $entity
251
     *
252
     * @throws ConfigException
253
     * @throws \Doctrine\ORM\Query\QueryException
254
     * @throws \ReflectionException
255
     * @SuppressWarnings(PHPMD.StaticAccess)
256
     */
257
    protected function callEntityGettersAndAssertNotNull(EntityInterface $entity): void
258
    {
259
        $class         = $this->getTestedEntityFqn();
260
        $entityManager = $this->getEntityManager();
261
        $meta          = $entityManager->getClassMetadata($class);
262
        foreach ($meta->getFieldNames() as $fieldName) {
263
            $type   = PersisterHelper::getTypeOfField($fieldName, $meta, $entityManager)[0];
264
            $method = $this->getGetterNameForField($fieldName, $type);
265
            if (\ts\stringContains($method, '.')) {
266
                list($getEmbeddableMethod,) = explode('.', $method);
267
                $embeddable = $entity->$getEmbeddableMethod();
268
                self::assertInstanceOf(AbstractEmbeddableObject::class, $embeddable);
269
                continue;
270
            }
271
            $reflectionMethod = new \ReflectionMethod($entity, $method);
272
            if ($reflectionMethod->hasReturnType()) {
273
                $returnType = $reflectionMethod->getReturnType();
274
                $allowsNull = $returnType->allowsNull();
275
                if ($allowsNull) {
276
                    // As we can't assert anything here so simply call
277
                    // the method and allow the type hint to raise any
278
                    // errors.
279
                    $entity->$method();
280
                    continue;
281
                }
282
                self::assertNotNull($entity->$method(), "$fieldName getter returned null");
283
                continue;
284
            }
285
            // If there is no return type then we can't assert anything,
286
            // but again we can just call the getter to check for errors
287
            $entity->$method();
288
        }
289
        if (0 === $this->getCount()) {
290
            self::markTestSkipped('No assertable getters in this Entity');
291
        }
292
    }
293
294
    /**
295
     * Generate a new entity and then update our Entity with the values from the generated one
296
     *
297
     * @param EntityInterface $entity
298
     *
299
     * @throws ConfigException
300
     * @throws \Doctrine\ORM\Mapping\MappingException
301
     * @throws \Doctrine\ORM\Query\QueryException
302
     * @throws \ReflectionException
303
     * @SuppressWarnings(PHPMD.StaticAccess)
304
     */
305
    protected function updateEntityFields(EntityInterface $entity): void
306
    {
307
        $class         = $this->getTestedEntityFqn();
308
        $entityManager = $this->getEntityManager();
309
        $meta          = $entityManager->getClassMetadata($class);
310
        $entityManager = $this->getEntityManager();
311
        $class         = $this->getTestedEntityFqn();
312
        $generated     = $this->testEntityGenerator->generateEntity($entityManager, $class, 10);
313
        $identifiers   = \array_flip($meta->getIdentifier());
314
        foreach ($meta->getFieldNames() as $fieldName) {
315
            if (isset($identifiers[$fieldName])) {
316
                continue;
317
            }
318
            if (true === $this->isUniqueField($meta, $fieldName)) {
319
                continue;
320
            }
321
            $setter = 'set' . $fieldName;
322
            if (!\method_exists($entity, $setter)) {
323
                continue;
324
            }
325
            $type   = PersisterHelper::getTypeOfField($fieldName, $meta, $entityManager)[0];
326
            $getter = $this->getGetterNameForField($fieldName, $type);
327
            if (\ts\stringContains($getter, '.')) {
328
                list($getEmbeddableMethod, $fieldInEmbeddable) = explode('.', $getter);
329
                $getterInEmbeddable  = 'get' . $fieldInEmbeddable;
330
                $setterInEmbeddable  = 'set' . $fieldInEmbeddable;
331
                $generatedEmbeddable = $generated->$getEmbeddableMethod();
332
                $embeddable          = $entity->$getEmbeddableMethod();
333
                if (\method_exists($embeddable, $setterInEmbeddable)
334
                    && \method_exists($embeddable, $getterInEmbeddable)
335
                ) {
336
                    $embeddable->$setterInEmbeddable($generatedEmbeddable->$getterInEmbeddable());
337
                }
338
                continue;
339
            }
340
            $entity->$setter($generated->$getter());
341
        }
342
    }
343
344
    protected function isUniqueField(ClassMetadata $meta, string $fieldName): bool
345
    {
346
        $fieldMapping = $meta->getFieldMapping($fieldName);
347
348
        return array_key_exists('unique', $fieldMapping) && true === $fieldMapping['unique'];
349
    }
350
351
    /**
352
     * Test that we have correctly generated an instance of our test entity
353
     *
354
     * @throws ConfigException
355
     * @throws \Doctrine\ORM\Query\QueryException
356
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
357
     * @throws \Exception
358
     * @throws \ReflectionException
359
     * @SuppressWarnings(PHPMD.StaticAccess)
360
     */
361
    public function testGeneratedCreate(): EntityInterface
362
    {
363
        $entityManager = $this->getEntityManager();
364
        $class         = $this->getTestedEntityFqn();
365
        $generated     = $this->testEntityGenerator->generateEntity($entityManager, $class);
366
        self::assertInstanceOf($class, $generated);
367
        $this->testEntityGenerator->addAssociationEntities($entityManager, $generated);
368
        $this->validateEntity($generated);
369
        $this->callEntityGettersAndAssertNotNull($generated);
370
        $this->entitySaverFactory->getSaverForEntity($generated)->save($generated);
371
372
        return $generated;
373
    }
374
375
    /**
376
     * Test that we can load the entity and then get and set
377
     *
378
     * @param EntityInterface $entity
379
     *
380
     * @return EntityInterface|null
381
     * @throws ConfigException
382
     * @throws \Doctrine\ORM\Mapping\MappingException
383
     * @throws \Doctrine\ORM\Query\QueryException
384
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
385
     * @throws \ReflectionException
386
     * @depends testGeneratedCreate
387
     */
388
    public function testLoadedEntity(EntityInterface $entity): EntityInterface
389
    {
390
        $class         = $this->getTestedEntityFqn();
391
        $entityManager = $this->getEntityManager();
392
        $loaded        = $this->loadEntity($class, $entity->getId(), $entityManager);
393
        self::assertSame($entity->getId(), $loaded->getId());
394
        self::assertInstanceOf($class, $loaded);
395
        $this->updateEntityFields($loaded);
396
        $this->assertAllAssociationsAreNotEmpty($loaded);
397
        $this->validateEntity($loaded);
398
        $this->removeAllAssociations($loaded);
399
        $this->assertAllAssociationsAreEmpty($loaded);
400
        $this->entitySaverFactory->getSaverForEntity($loaded)->save($loaded);
401
402
        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...
403
    }
404
405
    protected function dump(EntityInterface $entity): string
406
    {
407
        return $this->dumper->dump($entity, $this->getEntityManager());
408
    }
409
410
    /**
411
     * @param EntityInterface $entity
412
     *
413
     * @throws ConfigException
414
     * @depends testLoadedEntity
415
     */
416
    public function testReloadedEntityHasNoAssociations(EntityInterface $entity): void
417
    {
418
        $class         = $this->getTestedEntityFqn();
419
        $entityManager = $this->getEntityManager();
420
        $reLoaded      = $this->loadEntity($class, $entity->getId(), $entityManager);
421
        $entityDump    = $this->dump($entity);
422
        $reLoadedDump  = $this->dump($reLoaded);
423
        self::assertEquals($entityDump, $reLoadedDump);
424
        $this->assertAllAssociationsAreEmpty($reLoaded);
425
    }
426
427
    protected function assertAllAssociationsAreNotEmpty(EntityInterface $entity)
428
    {
429
        $entityManager = $this->getEntityManager();
430
        $class         = $this->getTestedEntityFqn();
431
        $meta          = $entityManager->getClassMetadata($class);
432
        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

432
        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...
433
            $getter = 'get' . $mapping['fieldName'];
434
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
435
                $collection = $entity->$getter()->toArray();
436
                $this->assertCorrectMappings($class, $mapping, $entityManager);
437
                self::assertNotEmpty(
438
                    $collection,
439
                    'Failed to load the collection of the associated entity [' . $mapping['fieldName']
440
                    . '] from the generated ' . $class
441
                    . ', make sure you have reciprocal adding of the association'
442
                );
443
444
                continue;
445
            }
446
            $association = $entity->$getter();
447
            self::assertNotEmpty(
448
                $association,
449
                'Failed to load the associated entity: [' . $mapping['fieldName']
450
                . '] from the generated ' . $class
451
            );
452
            self::assertNotEmpty(
453
                $association->getId(),
454
                'Failed to get the ID of the associated entity: [' . $mapping['fieldName']
455
                . '] from the generated ' . $class
456
            );
457
        }
458
    }
459
460
    /**
461
     * @param EntityInterface $entity
462
     *
463
     * @throws ConfigException
464
     * @SuppressWarnings(PHPMD.StaticAccess)
465
     */
466
    protected function removeAllAssociations(EntityInterface $entity)
467
    {
468
        $entityManager = $this->getEntityManager();
469
        $class         = $this->getTestedEntityFqn();
470
        $meta          = $entityManager->getClassMetadata($class);
471
        $identifiers   = array_flip($meta->getIdentifier());
472
        foreach ($meta->getAssociationMappings() as $mapping) {
473
            if (isset($identifiers[$mapping['fieldName']])) {
474
                continue;
475
            }
476
            $remover = 'remove' . Inflector::singularize($mapping['fieldName']);
477
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
478
                $getter    = 'get' . $mapping['fieldName'];
479
                $relations = $entity->$getter();
480
                foreach ($relations as $relation) {
481
                    $entity->$remover($relation);
482
                }
483
                continue;
484
            }
485
            $entity->$remover();
486
        }
487
        $this->assertAllAssociationsAreEmpty($entity);
488
    }
489
490
    protected function assertAllAssociationsAreEmpty(EntityInterface $entity)
491
    {
492
        $entityManager = $this->getEntityManager();
493
        $class         = $this->getTestedEntityFqn();
494
        $meta          = $entityManager->getClassMetadata($class);
495
        $identifiers   = array_flip($meta->getIdentifier());
496
        foreach ($meta->getAssociationMappings() as $mapping) {
497
            if (isset($identifiers[$mapping['fieldName']])) {
498
                continue;
499
            }
500
501
            $getter = 'get' . $mapping['fieldName'];
502
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
503
                $collection = $entity->$getter()->toArray();
504
                self::assertEmpty(
505
                    $collection,
506
                    'Collection of the associated entity [' . $mapping['fieldName']
507
                    . '] is not empty after calling remove'
508
                );
509
                continue;
510
            }
511
            $association = $entity->$getter();
512
            self::assertEmpty(
513
                $association,
514
                'Failed to remove associated entity: [' . $mapping['fieldName']
515
                . '] from the generated ' . $class
516
            );
517
        }
518
    }
519
520
    /**
521
     * @depends testConstructor
522
     *
523
     * @param EntityInterface $entity
524
     */
525
    public function testGetGetters(EntityInterface $entity)
526
    {
527
        $getters = $entity::getDoctrineStaticMeta()->getGetters();
528
        self::assertNotEmpty($getters);
529
        foreach ($getters as $getter) {
530
            self::assertRegExp('%^(get|is|has).+%', $getter);
531
        }
532
    }
533
534
    /**
535
     * @depends testConstructor
536
     *
537
     * @param EntityInterface $entity
538
     */
539
    public function testSetSetters(EntityInterface $entity)
540
    {
541
        $setters = $entity::getDoctrineStaticMeta()->getSetters();
542
        self::assertNotEmpty($setters);
543
        foreach ($setters as $setter) {
544
            self::assertRegExp('%^(set|add).+%', $setter);
545
        }
546
    }
547
548
549
    protected function getGetterNameForField(string $fieldName, string $type): string
550
    {
551
        if ($type === 'boolean') {
552
            return $this->codeHelper->getGetterMethodNameForBoolean($fieldName);
553
        }
554
555
        return 'get' . $fieldName;
556
    }
557
558
    /**
559
     * Loop through entity fields and find unique ones
560
     *
561
     * Then ensure that the unique rule is being enforced as expected
562
     *
563
     * @throws ConfigException
564
     * @throws \Doctrine\ORM\Mapping\MappingException
565
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
566
     * @throws \ReflectionException
567
     * @throws \Doctrine\ORM\Mapping\MappingException
568
     */
569
    public function testUniqueFieldsMustBeUnique(): void
570
    {
571
        $class         = $this->getTestedEntityFqn();
572
        $entityManager = $this->getEntityManager();
573
        $meta          = $entityManager->getClassMetadata($class);
574
        $uniqueFields  = [];
575
        foreach ($meta->getFieldNames() as $fieldName) {
576
            if (true === $this->isUniqueField($meta, $fieldName)) {
577
                $uniqueFields[] = $fieldName;
578
            }
579
        }
580
        if ([] === $uniqueFields) {
581
            self::markTestSkipped('No unique fields to check');
582
583
            return;
584
        }
585
        foreach ($uniqueFields as $fieldName) {
586
            $primary      = $this->testEntityGenerator->generateEntity($entityManager, $class);
587
            $secondary    = $this->testEntityGenerator->generateEntity($entityManager, $class);
588
            $getter       = 'get' . $fieldName;
589
            $setter       = 'set' . $fieldName;
590
            $primaryValue = $primary->$getter();
591
            $secondary->$setter($primaryValue);
592
            $saver = $this->entitySaverFactory->getSaverForEntity($primary);
593
            $this->expectException(UniqueConstraintViolationException::class);
594
            $saver->saveAll([$primary, $secondary]);
595
        }
596
    }
597
598
    /**
599
     * Check the mapping of our class and the associated entity to make sure it's configured properly on both sides.
600
     * Very easy to get wrong. This is in addition to the standard Schema Validation
601
     *
602
     * @param string                 $classFqn
603
     * @param array                  $mapping
604
     * @param EntityManagerInterface $entityManager
605
     */
606
    protected function assertCorrectMappings(string $classFqn, array $mapping, EntityManagerInterface $entityManager)
607
    {
608
        $pass                                 = false;
609
        $associationFqn                       = $mapping['targetEntity'];
610
        $associationMeta                      = $entityManager->getClassMetadata($associationFqn);
611
        $classTraits                          = $entityManager->getClassMetadata($classFqn)
612
                                                              ->getReflectionClass()
613
                                                              ->getTraits();
614
        $unidirectionalTraitShortNamePrefixes = [
615
            'Has' . $associationFqn::getDoctrineStaticMeta()->getSingular() . RelationsGenerator::PREFIX_UNIDIRECTIONAL,
616
            'Has' . $associationFqn::getDoctrineStaticMeta()->getPlural() . RelationsGenerator::PREFIX_UNIDIRECTIONAL,
617
        ];
618
        foreach ($classTraits as $trait) {
619
            foreach ($unidirectionalTraitShortNamePrefixes as $namePrefix) {
620
                if (0 === \stripos($trait->getShortName(), $namePrefix)) {
621
                    return;
622
                }
623
            }
624
        }
625
        foreach ($associationMeta->getAssociationMappings() as $associationMapping) {
626
            if ($classFqn === $associationMapping['targetEntity']) {
627
                $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

627
                /** @scrutinizer ignore-call */ 
628
                $pass = self::assertCorrectMapping($mapping, $associationMapping, $classFqn);
Loading history...
628
                break;
629
            }
630
        }
631
        self::assertTrue(
632
            $pass,
633
            'Failed finding association mapping to test for ' . "\n" . $mapping['targetEntity']
634
        );
635
    }
636
637
    /**
638
     * @param array  $mapping
639
     * @param array  $associationMapping
640
     * @param string $classFqn
641
     *
642
     * @return bool
643
     */
644
    protected function assertCorrectMapping(array $mapping, array $associationMapping, string $classFqn): bool
645
    {
646
        if (empty($mapping['joinTable'])) {
647
            self::assertArrayNotHasKey(
648
                'joinTable',
649
                $associationMapping,
650
                $classFqn . ' join table is empty,
651
                        but association ' . $mapping['targetEntity'] . ' join table is not empty'
652
            );
653
654
            return true;
655
        }
656
        self::assertNotEmpty(
657
            $associationMapping['joinTable'],
658
            "$classFqn joinTable is set to " . $mapping['joinTable']['name']
659
            . " \n association " . $mapping['targetEntity'] . ' join table is empty'
660
        );
661
        self::assertSame(
662
            $mapping['joinTable']['name'],
663
            $associationMapping['joinTable']['name'],
664
            "join tables not the same: \n * $classFqn = " . $mapping['joinTable']['name']
665
            . " \n * association " . $mapping['targetEntity']
666
            . ' = ' . $associationMapping['joinTable']['name']
667
        );
668
        self::assertArrayHasKey(
669
            'inverseJoinColumns',
670
            $associationMapping['joinTable'],
671
            "join table join columns not the same: \n * $classFqn joinColumn = "
672
            . $mapping['joinTable']['joinColumns'][0]['name']
673
            . " \n * association " . $mapping['targetEntity']
674
            . ' inverseJoinColumn is not set'
675
        );
676
        self::assertSame(
677
            $mapping['joinTable']['joinColumns'][0]['name'],
678
            $associationMapping['joinTable']['inverseJoinColumns'][0]['name'],
679
            "join table join columns not the same: \n * $classFqn joinColumn = "
680
            . $mapping['joinTable']['joinColumns'][0]['name']
681
            . " \n * association " . $mapping['targetEntity']
682
            . ' inverseJoinColumn = ' . $associationMapping['joinTable']['inverseJoinColumns'][0]['name']
683
        );
684
        self::assertSame(
685
            $mapping['joinTable']['inverseJoinColumns'][0]['name'],
686
            $associationMapping['joinTable']['joinColumns'][0]['name'],
687
            "join table join columns  not the same: \n * $classFqn inverseJoinColumn = "
688
            . $mapping['joinTable']['inverseJoinColumns'][0]['name']
689
            . " \n * association " . $mapping['targetEntity'] . ' joinColumn = '
690
            . $associationMapping['joinTable']['joinColumns'][0]['name']
691
        );
692
693
        return true;
694
    }
695
696
697
    protected function validateEntity(EntityInterface $entity): void
698
    {
699
        $entity->validate();
700
    }
701
702
703
    /**
704
     * Get the fully qualified name of the Entity we are testing,
705
     * assumes EntityNameTest as the entity class short name
706
     *
707
     * @return string
708
     */
709
    protected function getTestedEntityFqn(): string
710
    {
711
        if (null === $this->testedEntityFqn) {
712
            $this->testedEntityFqn = \substr(static::class, 0, -4);
713
        }
714
715
        return $this->testedEntityFqn;
716
    }
717
718
719
    /**
720
     * Get a \ReflectionClass for the currently tested Entity
721
     *
722
     * @return \ts\Reflection\ReflectionClass
723
     * @throws \ReflectionException
724
     */
725
    protected function getTestedEntityReflectionClass(): \ts\Reflection\ReflectionClass
726
    {
727
        if (null === $this->testedEntityReflectionClass) {
728
            $this->testedEntityReflectionClass = new \ts\Reflection\ReflectionClass(
729
                $this->getTestedEntityFqn()
730
            );
731
        }
732
733
        return $this->testedEntityReflectionClass;
734
    }
735
}
736