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 ( 833734...2baa6d )
by joseph
14s queued 12s
created

AbstractEntityTest   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 420
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 41
eloc 187
dl 0
loc 420
ccs 0
cts 197
cp 0
rs 9.1199
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A validateEntity() 0 4 1
A getGetterNameForField() 0 7 2
A testValidateSchema() 0 12 3
A getTestedEntityFqn() 0 7 2
B testGeneratedCreate() 0 64 7
A testConstructor() 0 5 1
A assertCorrectMappings() 0 26 6
A getEntityManager() 0 16 4
A setup() 0 16 1
A assertCorrectMapping() 0 50 2
A loadEntity() 0 3 1
A getTestedEntityReflectionClass() 0 9 2
A getSchemaErrors() 0 9 3
A assertUniqueFieldsMustBeUnique() 0 24 6

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