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 ( 0a6f96...b0c756 )
by Ross
12s queued 10s
created

AbstractEntityTest::assertCorrectMappings()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 26
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 18
nc 10
nop 3
dl 0
loc 26
rs 8.439
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\DoctrineStaticMeta\Entity\Testing;
4
5
use Doctrine\Common\Cache\ArrayCache;
6
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
7
use Doctrine\ORM\EntityManager;
8
use Doctrine\ORM\Mapping\ClassMetadataInfo;
9
use Doctrine\ORM\Tools\SchemaValidator;
10
use Doctrine\ORM\Utility\PersisterHelper;
11
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator;
12
use EdmondsCommerce\DoctrineStaticMeta\Config;
13
use EdmondsCommerce\DoctrineStaticMeta\ConfigInterface;
14
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\EntityInterface;
15
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\Validation\EntityValidatorInterface;
16
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\EntitySaverFactory;
17
use EdmondsCommerce\DoctrineStaticMeta\Entity\Validation\EntityValidatorFactory;
18
use EdmondsCommerce\DoctrineStaticMeta\EntityManager\EntityManagerFactory;
19
use EdmondsCommerce\DoctrineStaticMeta\Exception\ConfigException;
20
use EdmondsCommerce\DoctrineStaticMeta\SimpleEnv;
21
use PHPUnit\Framework\TestCase;
22
use Symfony\Component\Validator\Mapping\Cache\DoctrineCache;
23
24
/**
25
 * Class AbstractEntityTest
26
 *
27
 * This abstract test is designed to give you a good level of test coverage for your entities without any work required.
28
 *
29
 * You should extend the test with methods that test your specific business logic, your validators and anything else.
30
 *
31
 * You can override the methods, properties and constants as you see fit.
32
 *
33
 * @package EdmondsCommerce\DoctrineStaticMeta\Entity
34
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
35
 */
36
abstract class AbstractEntityTest extends TestCase implements EntityTestInterface
37
{
38
    /**
39
     * The fully qualified name of the Entity being tested, as calculated by the test class name
40
     *
41
     * @var string
42
     */
43
    protected $testedEntityFqn;
44
45
    /**
46
     * Reflection of the tested entity
47
     *
48
     * @var \ReflectionClass
49
     */
50
    protected $testedEntityReflectionClass;
51
52
53
    /**
54
     * @var EntityValidatorInterface
55
     */
56
    protected $entityValidator;
57
58
    /**
59
     * @var EntityManager
60
     */
61
    protected $entityManager;
62
63
    /**
64
     * @var array
65
     */
66
    protected $schemaErrors = [];
67
68
69
    /**
70
     * @var TestEntityGenerator
71
     */
72
    protected $testEntityGenerator;
73
74
    /**
75
     * @var EntitySaverFactory
76
     */
77
    protected $entitySaverFactory;
78
79
80
    /**
81
     * @throws ConfigException
82
     * @throws \Exception
83
     * @SuppressWarnings(PHPMD.StaticAccess)
84
     */
85
    protected function setup()
86
    {
87
        $this->getEntityManager(true);
88
        $this->entityValidator     = (
89
        new EntityValidatorFactory(new DoctrineCache(new ArrayCache()))
90
        )->getEntityValidator();
91
        $this->entitySaverFactory  = new EntitySaverFactory($this->entityManager);
92
        $this->testEntityGenerator = new TestEntityGenerator(
93
            $this->entityManager,
94
            (float)static::SEED,
95
            static::FAKER_DATA_PROVIDERS,
96
            $this->getTestedEntityReflectionClass(),
97
            $this->entitySaverFactory
98
        );
99
    }
100
101
    /**
102
     * Use Doctrine's standard schema validation to get errors for the whole schema
103
     *
104
     * @param bool $update
105
     *
106
     * @return array
107
     * @throws \Exception
108
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
109
     */
110
    protected function getSchemaErrors(bool $update = false): array
111
    {
112
        if (empty($this->schemaErrors) || true === $update) {
113
            $entityManager      = $this->getEntityManager();
114
            $validator          = new SchemaValidator($entityManager);
115
            $this->schemaErrors = $validator->validateMapping();
116
        }
117
118
        return $this->schemaErrors;
119
    }
120
121
    /**
122
     * If a global function dsmGetEntityManagerFactory is defined, we use this
123
     *
124
     * Otherwise, we use the standard DevEntityManagerFactory,
125
     * we define a DB name which is the main DB from env but with `_test` suffixed
126
     *
127
     * @param bool $new
128
     *
129
     * @return EntityManager
130
     * @throws ConfigException
131
     * @throws \Exception
132
     * @SuppressWarnings(PHPMD)
133
     */
134
    protected function getEntityManager(bool $new = false): EntityManager
135
    {
136
        if (null === $this->entityManager || true === $new) {
137
            if (\function_exists(self::GET_ENTITY_MANAGER_FUNCTION_NAME)) {
138
                $this->entityManager = \call_user_func(self::GET_ENTITY_MANAGER_FUNCTION_NAME);
139
            } else {
140
                SimpleEnv::setEnv(Config::getProjectRootDirectory().'/.env');
141
                $testConfig                                 = $_SERVER;
142
                $testConfig[ConfigInterface::PARAM_DB_NAME] = $_SERVER[ConfigInterface::PARAM_DB_NAME].'_test';
143
                $config                                     = new Config($testConfig);
144
                $this->entityManager                        = (new EntityManagerFactory(new ArrayCache()))
145
                    ->getEntityManager($config);
146
            }
147
        }
148
149
        return $this->entityManager;
150
    }
151
152
153
    /**
154
     * Use Doctrine's built in schema validation tool to catch issues
155
     */
156
    public function testValidateSchema()
157
    {
158
        $errors  = $this->getSchemaErrors();
159
        $class   = $this->getTestedEntityFqn();
160
        $message = '';
161
        if (isset($errors[$class])) {
162
            $message = "Failed ORM Validate Schema:\n";
163
            foreach ($errors[$class] as $err) {
164
                $message .= "\n * $err \n";
165
            }
166
        }
167
        $this->assertEmpty($message);
168
    }
169
170
171
    /**
172
     * @param string        $class
173
     * @param int|string    $id
174
     * @param EntityManager $entityManager
175
     *
176
     * @return EntityInterface|null
177
     */
178
    protected function loadEntity(string $class, $id, EntityManager $entityManager): ?EntityInterface
179
    {
180
        return $entityManager->getRepository($class)->find($id);
181
    }
182
183
    /**
184
     * Test that we have correctly generated an instance of our test entity
185
     *
186
     * @throws ConfigException
187
     * @throws \Doctrine\ORM\Query\QueryException
188
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
189
     * @throws \Exception
190
     * @throws \ReflectionException
191
     * @SuppressWarnings(PHPMD.StaticAccess)
192
     */
193
    public function testGeneratedCreate()
194
    {
195
        $entityManager = $this->getEntityManager();
196
        $class         = $this->getTestedEntityFqn();
197
        $generated     = $this->testEntityGenerator->generateEntity($class);
198
        $this->assertInstanceOf($class, $generated);
199
        $saver = $this->entitySaverFactory->getSaverForEntity($generated);
200
        $this->testEntityGenerator->addAssociationEntities($entityManager, $generated);
201
        $this->validateEntity($generated);
202
        $meta = $entityManager->getClassMetadata($class);
203
        foreach ($meta->getFieldNames() as $fieldName) {
204
            $type             = PersisterHelper::getTypeOfField($fieldName, $meta, $entityManager);
205
            $method           = ($type[0] === 'boolean' ? 'is' : 'get').$fieldName;
206
            $reflectionMethod = new \ReflectionMethod($generated, $method);
207
            if ($reflectionMethod->hasReturnType()) {
208
                $returnType = $reflectionMethod->getReturnType();
209
                $allowsNull = $returnType->allowsNull();
210
                if ($allowsNull) {
211
                    // As we can't assert anything here so simply call
212
                    // the method and allow the type hint to raise any
213
                    // errors.
214
                    $generated->$method();
215
                    continue;
216
                }
217
            }
218
            $this->assertNotEmpty($generated->$method(), "$fieldName getter returned empty");
219
        }
220
        $saver->save($generated);
221
        $entityManager = $this->getEntityManager(true);
222
        $loaded        = $this->loadEntity($class, $generated->getId(), $entityManager);
223
        $this->assertInstanceOf($class, $loaded);
224
        $this->validateEntity($loaded);
225
        foreach ($meta->getAssociationMappings() as $mapping) {
226
            $getter = 'get'.$mapping['fieldName'];
227
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
228
                $collection = $loaded->$getter()->toArray();
229
                $this->assertNotEmpty(
230
                    $collection,
231
                    'Failed to load the collection of the associated entity ['.$mapping['fieldName']
232
                    .'] from the generated '.$class
233
                    .', make sure you have reciprocal adding of the association'
234
                );
235
                $this->assertCorrectMappings($class, $mapping, $entityManager);
236
                continue;
237
            }
238
            $association = $loaded->$getter();
239
            $this->assertNotEmpty(
240
                $association,
241
                'Failed to load the associated entity: ['.$mapping['fieldName']
242
                .'] from the generated '.$class
243
            );
244
            $this->assertNotEmpty(
245
                $association->getId(),
246
                'Failed to get the ID of the associated entity: ['.$mapping['fieldName']
247
                .'] from the generated '.$class
248
            );
249
        }
250
        $this->assertUniqueFieldsMustBeUnique($meta);
251
    }
252
253
    /**
254
     * Loop through entity fields and find unique ones
255
     *
256
     * Then ensure that the unique rule is being enforced as expected
257
     *
258
     * @param ClassMetadataInfo $meta
259
     *
260
     * @throws ConfigException
261
     * @throws \Doctrine\ORM\Mapping\MappingException
262
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
263
     * @throws \ReflectionException
264
     * @throws \Doctrine\ORM\Mapping\MappingException
265
     */
266
    protected function assertUniqueFieldsMustBeUnique(ClassMetadataInfo $meta): void
267
    {
268
        $uniqueFields = [];
269
        foreach ($meta->getFieldNames() as $fieldName) {
270
            $fieldMapping = $meta->getFieldMapping($fieldName);
271
            if (array_key_exists('unique', $fieldMapping) && true === $fieldMapping['unique']) {
272
                $uniqueFields[$fieldName] = $fieldMapping;
273
            }
274
        }
275
        if ([] === $uniqueFields) {
276
            return;
277
        }
278
        $class = $this->getTestedEntityFqn();
279
        foreach ($uniqueFields as $fieldName => $fieldMapping) {
280
            $primary      = $this->testEntityGenerator->generateEntity($class);
281
            $secondary    = $this->testEntityGenerator->generateEntity($class);
282
            $getter       = 'get'.$fieldName;
283
            $setter       = 'set'.$fieldName;
284
            $primaryValue = $primary->$getter();
285
            $secondary->$setter($primaryValue);
286
            $saver = $this->entitySaverFactory->getSaverForEntity($primary);
287
            $this->expectException(UniqueConstraintViolationException::class);
288
            $saver->saveAll([$primary, $secondary]);
289
        }
290
    }
291
292
    /**
293
     * Check the mapping of our class and the associated entity to make sure it's configured properly on both sides.
294
     * Very easy to get wrong. This is in addition to the standard Schema Validation
295
     *
296
     * @param string        $classFqn
297
     * @param array         $mapping
298
     * @param EntityManager $entityManager
299
     */
300
    protected function assertCorrectMappings(string $classFqn, array $mapping, EntityManager $entityManager)
301
    {
302
        $pass                                 = false;
303
        $associationFqn                       = $mapping['targetEntity'];
304
        $associationMeta                      = $entityManager->getClassMetadata($associationFqn);
305
        $classTraits                          = $entityManager->getClassMetadata($classFqn)
306
                                                              ->getReflectionClass()
307
                                                              ->getTraits();
308
        $unidirectionalTraitShortNamePrefixes = [
309
            'Has'.$associationFqn::getSingular().RelationsGenerator::PREFIX_UNIDIRECTIONAL,
310
            'Has'.$associationFqn::getPlural().RelationsGenerator::PREFIX_UNIDIRECTIONAL,
311
        ];
312
        foreach ($classTraits as $trait) {
313
            foreach ($unidirectionalTraitShortNamePrefixes as $namePrefix) {
314
                if (0 === \stripos($trait->getShortName(), $namePrefix)) {
315
                    return;
316
                }
317
            }
318
        }
319
        foreach ($associationMeta->getAssociationMappings() as $associationMapping) {
320
            if ($classFqn === $associationMapping['targetEntity']) {
321
                $pass = $this->assertCorrectMapping($mapping, $associationMapping, $classFqn);
322
                break;
323
            }
324
        }
325
        $this->assertTrue($pass, 'Failed finding association mapping to test for '."\n".$mapping['targetEntity']);
326
    }
327
328
    /**
329
     * @param array  $mapping
330
     * @param array  $associationMapping
331
     * @param string $classFqn
332
     *
333
     * @return bool
334
     */
335
    protected function assertCorrectMapping(array $mapping, array $associationMapping, string $classFqn)
336
    {
337
        if (empty($mapping['joinTable'])) {
338
            $this->assertArrayNotHasKey(
339
                'joinTable',
340
                $associationMapping,
341
                $classFqn.' join table is empty,
342
                        but association '.$mapping['targetEntity'].' join table is not empty'
343
            );
344
345
            return true;
346
        }
347
        $this->assertNotEmpty(
348
            $associationMapping['joinTable'],
349
            "$classFqn joinTable is set to ".$mapping['joinTable']['name']
350
            ." \n association ".$mapping['targetEntity'].' join table is empty'
351
        );
352
        $this->assertSame(
353
            $mapping['joinTable']['name'],
354
            $associationMapping['joinTable']['name'],
355
            "join tables not the same: \n * $classFqn = ".$mapping['joinTable']['name']
356
            ." \n * association ".$mapping['targetEntity']
357
            .' = '.$associationMapping['joinTable']['name']
358
        );
359
        $this->assertArrayHasKey(
360
            'inverseJoinColumns',
361
            $associationMapping['joinTable'],
362
            "join table join columns not the same: \n * $classFqn joinColumn = "
363
            .$mapping['joinTable']['joinColumns'][0]['name']
364
            ." \n * association ".$mapping['targetEntity']
365
            .' inverseJoinColumn is not set'
366
        );
367
        $this->assertSame(
368
            $mapping['joinTable']['joinColumns'][0]['name'],
369
            $associationMapping['joinTable']['inverseJoinColumns'][0]['name'],
370
            "join table join columns not the same: \n * $classFqn joinColumn = "
371
            .$mapping['joinTable']['joinColumns'][0]['name']
372
            ." \n * association ".$mapping['targetEntity']
373
            .' inverseJoinColumn = '.$associationMapping['joinTable']['inverseJoinColumns'][0]['name']
374
        );
375
        $this->assertSame(
376
            $mapping['joinTable']['inverseJoinColumns'][0]['name'],
377
            $associationMapping['joinTable']['joinColumns'][0]['name'],
378
            "join table join columns  not the same: \n * $classFqn inverseJoinColumn = "
379
            .$mapping['joinTable']['inverseJoinColumns'][0]['name']
380
            ." \n * association ".$mapping['targetEntity'].' joinColumn = '
381
            .$associationMapping['joinTable']['joinColumns'][0]['name']
382
        );
383
384
        return true;
385
    }
386
387
388
    protected function validateEntity(EntityInterface $entity): void
389
    {
390
        $entity->injectValidator($this->entityValidator);
391
        $entity->validate();
392
    }
393
394
395
    /**
396
     * Get the fully qualified name of the Entity we are testing,
397
     * assumes EntityNameTest as the entity class short name
398
     *
399
     * @return string
400
     */
401
    protected function getTestedEntityFqn(): string
402
    {
403
        if (null === $this->testedEntityFqn) {
404
            $this->testedEntityFqn = \substr(static::class, 0, -4);
405
        }
406
407
        return $this->testedEntityFqn;
408
    }
409
410
411
    /**
412
     * Get a \ReflectionClass for the currently tested Entity
413
     *
414
     * @return \ReflectionClass
415
     * @throws \ReflectionException
416
     */
417
    protected function getTestedEntityReflectionClass(): \ReflectionClass
418
    {
419
        if (null === $this->testedEntityReflectionClass) {
420
            $this->testedEntityReflectionClass = new \ReflectionClass(
421
                $this->getTestedEntityFqn()
422
            );
423
        }
424
425
        return $this->testedEntityReflectionClass;
426
    }
427
}
428