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 ( 8619f1...53d2ad )
by Ross
52s queued 13s
created

AbstractEntityTest   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 509
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 509
rs 8.439
c 0
b 0
f 0
wmc 47

16 Methods

Rating   Name   Duplication   Size   Complexity  
B testGeneratedCreate() 0 55 7
A getTestedEntityFqn() 0 7 2
A generateEntity() 0 10 1
C assertCorrectMapping() 0 72 7
A validateEntity() 0 4 1
A loadEntity() 0 3 1
A getSaverFqn() 0 13 1
A getSaver() 0 7 1
A setFakerDataProvider() 0 10 3
A getSchemaErrors() 0 9 3
A getTestedEntityReflectionClass() 0 9 2
A setup() 0 9 2
B addAssociationEntities() 0 41 4
A getEntityManager() 0 16 4
A testValidateSchema() 0 12 3
B generateColumnFormatters() 0 20 5

How to fix   Complexity   

Complex Class

Complex classes like AbstractEntityTest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractEntityTest, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\DoctrineStaticMeta\Entity;
4
5
use Doctrine\Common\Cache\ArrayCache;
6
use Doctrine\Common\Collections\ArrayCollection;
7
use Doctrine\ORM\EntityManager;
8
use Doctrine\ORM\Tools\SchemaValidator;
9
use Doctrine\ORM\Utility\PersisterHelper;
10
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator;
11
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\NamespaceHelper;
12
use EdmondsCommerce\DoctrineStaticMeta\Config;
13
use EdmondsCommerce\DoctrineStaticMeta\ConfigInterface;
14
use EdmondsCommerce\DoctrineStaticMeta\Entity\Fields\FakerData\Attribute\IpAddressFakerData;
15
use EdmondsCommerce\DoctrineStaticMeta\Entity\Fields\Interfaces\Attribute\IpAddressFieldInterface;
16
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\EntityInterface;
17
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\Validation\EntityValidatorInterface;
18
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\AbstractSaver;
19
use EdmondsCommerce\DoctrineStaticMeta\Entity\Validation\EntityValidatorFactory;
20
use EdmondsCommerce\DoctrineStaticMeta\EntityManager\EntityManagerFactory;
21
use EdmondsCommerce\DoctrineStaticMeta\Exception\ConfigException;
22
use EdmondsCommerce\DoctrineStaticMeta\SimpleEnv;
23
use Faker;
24
use Faker\ORM\Doctrine\Populator;
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
 */
39
abstract class AbstractEntityTest extends AbstractTest
40
{
41
    public const GET_ENTITY_MANAGER_FUNCTION_NAME = 'dsmGetEntityManagerFactory';
42
43
    /**
44
     * The fully qualified name of the Entity being tested, as calculated by the test class name
45
     *
46
     * @var string
47
     */
48
    protected $testedEntityFqn;
49
50
    /**
51
     * Reflection of the tested entity
52
     *
53
     * @var \ReflectionClass
54
     */
55
    protected $testedEntityReflectionClass;
56
57
    /**
58
     * @var Faker\Generator
59
     */
60
    protected $generator;
61
62
    /**
63
     * @var EntityValidatorInterface
64
     */
65
    protected $entityValidator;
66
67
    /**
68
     * @var EntityManager
69
     */
70
    protected $entityManager;
71
72
    /**
73
     * @var array
74
     */
75
    protected $schemaErrors = [];
76
77
    /**
78
     * Standard library faker data provider FQNs
79
     *
80
     * This const should be overridden in your child class and extended with any project specific field data providers
81
     * in addition to the standard library
82
     *
83
     * The key is the column/property name and the value is the FQN for the data provider
84
     */
85
    public const FAKER_DATA_PROVIDERS = [
86
        IpAddressFieldInterface::PROP_IP_ADDRESS => IpAddressFakerData::class,
87
    ];
88
89
    /**
90
     * Faker can be seeded with a number which makes the generation deterministic
91
     * This helps to avoid tests that fail randomly
92
     * If you do want randomness, override this and set it to null
93
     */
94
    public const SEED = 100111991161141051101013211511697116105993210910111697;
95
96
    /**
97
     * A cache of instantiated column data providers
98
     *
99
     * @var array
100
     */
101
    protected $fakerDataProviderObjects = [];
102
103
    /**
104
     * @throws ConfigException
105
     * @throws \Exception
106
     * @SuppressWarnings(PHPMD.StaticAccess)
107
     */
108
    protected function setup()
109
    {
110
        $this->getEntityManager(true);
111
        $this->entityValidator = (
112
        new EntityValidatorFactory(new DoctrineCache(new ArrayCache()))
113
        )->getEntityValidator();
114
        $this->generator       = Faker\Factory::create();
115
        if (null !== static::SEED) {
0 ignored issues
show
introduced by
The condition null !== static::SEED is always true.
Loading history...
116
            $this->generator->seed(static::SEED);
117
        }
118
    }
119
120
    /**
121
     * Use Doctrine's standard schema validation to get errors for the whole schema
122
     *
123
     * @param bool $update
124
     *
125
     * @return array
126
     * @throws \Exception
127
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
128
     */
129
    protected function getSchemaErrors(bool $update = false): array
130
    {
131
        if (empty($this->schemaErrors) || true === $update) {
132
            $entityManager      = $this->getEntityManager();
133
            $validator          = new SchemaValidator($entityManager);
134
            $this->schemaErrors = $validator->validateMapping();
135
        }
136
137
        return $this->schemaErrors;
138
    }
139
140
    /**
141
     * If a global function dsmGetEntityManagerFactory is defined, we use this
142
     *
143
     * Otherwise, we use the standard DevEntityManagerFactory,
144
     * we define a DB name which is the main DB from env but with `_test` suffixed
145
     *
146
     * @param bool $new
147
     *
148
     * @return EntityManager
149
     * @throws ConfigException
150
     * @throws \Exception
151
     * @SuppressWarnings(PHPMD)
152
     */
153
    protected function getEntityManager(bool $new = false): EntityManager
154
    {
155
        if (null === $this->entityManager || true === $new) {
156
            if (\function_exists(self::GET_ENTITY_MANAGER_FUNCTION_NAME)) {
157
                $this->entityManager = \call_user_func(self::GET_ENTITY_MANAGER_FUNCTION_NAME);
158
            } else {
159
                SimpleEnv::setEnv(Config::getProjectRootDirectory().'/.env');
160
                $testConfig                                 = $_SERVER;
161
                $testConfig[ConfigInterface::PARAM_DB_NAME] = $_SERVER[ConfigInterface::PARAM_DB_NAME].'_test';
162
                $config                                     = new Config($testConfig);
163
                $this->entityManager                        = (new EntityManagerFactory(new ArrayCache()))
164
                    ->getEntityManager($config);
165
            }
166
        }
167
168
        return $this->entityManager;
169
    }
170
171
    /**
172
     * @param EntityManager   $entityManager
173
     * @param EntityInterface $entity
174
     *
175
     * @return AbstractSaver
176
     * @throws \ReflectionException
177
     */
178
    protected function getSaver(
179
        EntityManager $entityManager,
180
        EntityInterface $entity
181
    ): AbstractSaver {
182
        $saverFqn = $this->getSaverFqn($entity);
183
184
        return new $saverFqn($entityManager);
185
    }
186
187
    /**
188
     * Use Doctrine's built in schema validation tool to catch issues
189
     */
190
    public function testValidateSchema()
191
    {
192
        $errors  = $this->getSchemaErrors();
193
        $class   = $this->getTestedEntityFqn();
194
        $message = '';
195
        if (isset($errors[$class])) {
196
            $message = "Failed ORM Validate Schema:\n";
197
            foreach ($errors[$class] as $err) {
198
                $message .= "\n * $err \n";
199
            }
200
        }
201
        $this->assertEmpty($message);
202
    }
203
204
205
    /**
206
     * @param string        $class
207
     * @param int|string    $id
208
     * @param EntityManager $entityManager
209
     *
210
     * @return EntityInterface|null
211
     */
212
    protected function loadEntity(string $class, $id, EntityManager $entityManager): ?EntityInterface
213
    {
214
        return $entityManager->getRepository($class)->find($id);
215
    }
216
217
    /**
218
     * Test that we have correctly generated an instance of our test entity
219
     *
220
     * @throws ConfigException
221
     * @throws \Doctrine\ORM\Query\QueryException
222
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
223
     * @throws \Exception
224
     * @throws \ReflectionException
225
     * @SuppressWarnings(PHPMD.StaticAccess)
226
     */
227
    public function testGeneratedCreate()
228
    {
229
        $entityManager = $this->getEntityManager();
230
        $class         = $this->getTestedEntityFqn();
231
        $generated     = $this->generateEntity($class);
232
        $this->assertInstanceOf($class, $generated);
233
        $saver = $this->getSaver($entityManager, $generated);
234
        $this->addAssociationEntities($entityManager, $generated);
235
        $this->validateEntity($generated);
236
        $meta = $entityManager->getClassMetadata($class);
237
        foreach ($meta->getFieldNames() as $fieldName) {
238
            $type             = PersisterHelper::getTypeOfField($fieldName, $meta, $entityManager);
239
            $method           = ($type[0] === 'boolean' ? 'is' : 'get').$fieldName;
240
            $reflectionMethod = new \ReflectionMethod($generated, $method);
241
            if ($reflectionMethod->hasReturnType()) {
242
                $returnType = $reflectionMethod->getReturnType();
243
                $allowsNull = $returnType->allowsNull();
244
                if ($allowsNull) {
245
                    // As we can't assert anything here so simply call
246
                    // the method and allow the type hint to raise any
247
                    // errors.
248
                    $generated->$method();
249
                    continue;
250
                }
251
            }
252
            $this->assertNotEmpty($generated->$method(), "$fieldName getter returned empty");
253
        }
254
        $saver->save($generated);
255
        $entityManager = $this->getEntityManager(true);
256
        $loaded        = $this->loadEntity($class, $generated->getId(), $entityManager);
257
        $this->assertInstanceOf($class, $loaded);
258
        $this->validateEntity($loaded);
259
        foreach ($meta->getAssociationMappings() as $mapping) {
260
            $getter = 'get'.$mapping['fieldName'];
261
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
262
                $collection = $loaded->$getter()->toArray();
263
                $this->assertNotEmpty(
264
                    $collection,
265
                    'Failed to load the collection of the associated entity ['.$mapping['fieldName']
266
                    .'] from the generated '.$class
267
                    .', make sure you have reciprocal adding of the association'
268
                );
269
                $this->assertCorrectMapping($class, $mapping, $entityManager);
270
                continue;
271
            }
272
            $association = $loaded->$getter();
273
            $this->assertNotEmpty(
274
                $association,
275
                'Failed to load the associated entity: ['.$mapping['fieldName']
276
                .'] from the generated '.$class
277
            );
278
            $this->assertNotEmpty(
279
                $association->getId(),
280
                'Failed to get the ID of the associated entity: ['.$mapping['fieldName']
281
                .'] from the generated '.$class
282
            );
283
        }
284
    }
285
286
    /**
287
     * Check the mapping of our class and the associated entity to make sure it's configured properly on both sides.
288
     * Very easy to get wrong. This is in addition to the standard Schema Validation
289
     *
290
     * @param string        $classFqn
291
     * @param array         $mapping
292
     * @param EntityManager $entityManager
293
     */
294
    protected function assertCorrectMapping(string $classFqn, array $mapping, EntityManager $entityManager)
295
    {
296
        $pass                                 = false;
297
        $associationFqn                       = $mapping['targetEntity'];
298
        $associationMeta                      = $entityManager->getClassMetadata($associationFqn);
299
        $classTraits                          = $entityManager->getClassMetadata($classFqn)
300
                                                              ->getReflectionClass()
301
                                                              ->getTraits();
302
        $unidirectionalTraitShortNamePrefixes = [
303
            'Has'.$associationFqn::getSingular().RelationsGenerator::PREFIX_UNIDIRECTIONAL,
304
            'Has'.$associationFqn::getPlural().RelationsGenerator::PREFIX_UNIDIRECTIONAL,
305
        ];
306
        foreach ($classTraits as $trait) {
307
            foreach ($unidirectionalTraitShortNamePrefixes as $namePrefix) {
308
                if (0 === \stripos($trait->getShortName(), $namePrefix)) {
309
                    return;
310
                }
311
            }
312
        }
313
        foreach ($associationMeta->getAssociationMappings() as $associationMapping) {
314
            if ($classFqn === $associationMapping['targetEntity']) {
315
                if (empty($mapping['joinTable'])) {
316
                    $this->assertArrayNotHasKey(
317
                        'joinTable',
318
                        $associationMapping,
319
                        $classFqn.' join table is empty,
320
                        but association '.$mapping['targetEntity'].' join table is not empty'
321
                    );
322
                    $pass = true;
323
                    break;
324
                }
325
                $this->assertNotEmpty(
326
                    $associationMapping['joinTable'],
327
                    "$classFqn joinTable is set to ".$mapping['joinTable']['name']
328
                    ." \n association ".$mapping['targetEntity'].' join table is empty'
329
                );
330
                $this->assertSame(
331
                    $mapping['joinTable']['name'],
332
                    $associationMapping['joinTable']['name'],
333
                    "join tables not the same: \n * $classFqn = ".$mapping['joinTable']['name']
334
                    ." \n * association ".$mapping['targetEntity']
335
                    .' = '.$associationMapping['joinTable']['name']
336
                );
337
                $this->assertArrayHasKey(
338
                    'inverseJoinColumns',
339
                    $associationMapping['joinTable'],
340
                    "join table join columns not the same: \n * $classFqn joinColumn = "
341
                    .$mapping['joinTable']['joinColumns'][0]['name']
342
                    ." \n * association ".$mapping['targetEntity']
343
                    .' inverseJoinColumn is not set'
344
                );
345
                $this->assertSame(
346
                    $mapping['joinTable']['joinColumns'][0]['name'],
347
                    $associationMapping['joinTable']['inverseJoinColumns'][0]['name'],
348
                    "join table join columns not the same: \n * $classFqn joinColumn = "
349
                    .$mapping['joinTable']['joinColumns'][0]['name']
350
                    ." \n * association ".$mapping['targetEntity']
351
                    .' inverseJoinColumn = '.$associationMapping['joinTable']['inverseJoinColumns'][0]['name']
352
                );
353
                $this->assertSame(
354
                    $mapping['joinTable']['inverseJoinColumns'][0]['name'],
355
                    $associationMapping['joinTable']['joinColumns'][0]['name'],
356
                    "join table join columns  not the same: \n * $classFqn inverseJoinColumn = "
357
                    .$mapping['joinTable']['inverseJoinColumns'][0]['name']
358
                    ." \n * association ".$mapping['targetEntity'].' joinColumn = '
359
                    .$associationMapping['joinTable']['joinColumns'][0]['name']
360
                );
361
                $pass = true;
362
                break;
363
            }
364
        }
365
        $this->assertTrue($pass, 'Failed finding association mapping to test for '."\n".$mapping['targetEntity']);
366
    }
367
368
    /**
369
     * @param string $class
370
     *
371
     * @return EntityInterface
372
     * @throws ConfigException
373
     * @throws \Exception
374
     * @SuppressWarnings(PHPMD.StaticAccess)
375
     */
376
    protected function generateEntity(string $class): EntityInterface
377
    {
378
        $entityManager          = $this->getEntityManager();
379
        $customColumnFormatters = $this->generateColumnFormatters($entityManager, $class);
380
        $populator              = new Populator($this->generator, $entityManager);
381
        $populator->addEntity($class, 1, $customColumnFormatters);
382
383
        $entity = $populator->execute()[$class][0];
384
385
        return $entity;
386
    }
387
388
    protected function validateEntity(EntityInterface $entity): void
389
    {
390
        $entity->setValidator($this->entityValidator);
391
        $entity->validate();
392
    }
393
394
    /**
395
     * @param EntityManager $entityManager
396
     * @param string        $class
397
     *
398
     * @return array
399
     */
400
    protected function generateColumnFormatters(EntityManager $entityManager, string $class): array
401
    {
402
        $columnFormatters = [];
403
        $meta             = $entityManager->getClassMetadata($class);
404
        $mappings         = $meta->getAssociationMappings();
405
        foreach ($mappings as $mapping) {
406
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
407
                $columnFormatters[$mapping['fieldName']] = new ArrayCollection();
408
                continue;
409
            }
410
            $columnFormatters[$mapping['fieldName']] = null;
411
        }
412
        $columns = $meta->getColumnNames();
413
        foreach ($columns as $column) {
414
            if (!isset($columnFormatters[$column])) {
415
                $this->setFakerDataProvider($columnFormatters, $column);
416
            }
417
        }
418
419
        return $columnFormatters;
420
    }
421
422
    /**
423
     * Add a faker data provider to the columnFormatters array (by reference) if there is one available
424
     *
425
     * Handles instantiating and caching of the data providers
426
     *
427
     * @param array  $columnFormatters
428
     * @param string $column
429
     */
430
    protected function setFakerDataProvider(array &$columnFormatters, string $column): void
431
    {
432
        if (!isset(static::FAKER_DATA_PROVIDERS[$column])) {
433
            return;
434
        }
435
        if (!isset($this->fakerDataProviderObjects[$column])) {
436
            $class                                   = static::FAKER_DATA_PROVIDERS[$column];
437
            $this->fakerDataProviderObjects[$column] = new $class($this->generator);
438
        }
439
        $columnFormatters[$column] = $this->fakerDataProviderObjects[$column];
440
    }
441
442
    /**
443
     * @param EntityManager   $entityManager
444
     * @param EntityInterface $generated
445
     *
446
     * @throws ConfigException
447
     * @throws \Exception
448
     * @throws \ReflectionException
449
     * @SuppressWarnings(PHPMD.ElseExpression)
450
     */
451
    protected function addAssociationEntities(
452
        EntityManager $entityManager,
453
        EntityInterface $generated
454
    ) {
455
        $entityReflection = $this->getTestedEntityReflectionClass();
456
        $class            = $entityReflection->getName();
457
        $meta             = $entityManager->getClassMetadata($class);
458
        $mappings         = $meta->getAssociationMappings();
459
        if (empty($mappings)) {
460
            return;
461
        }
462
        $namespaceHelper = new NamespaceHelper();
463
        $methods         = array_map('strtolower', get_class_methods($generated));
464
        foreach ($mappings as $mapping) {
465
            $mappingEntityClass = $mapping['targetEntity'];
466
            $mappingEntity      = $this->generateEntity($mappingEntityClass);
467
            $errorMessage       = "Error adding association entity $mappingEntityClass to $class: %s";
468
            $saver              = $this->getSaver($entityManager, $mappingEntity);
469
            $saver->save($mappingEntity);
470
            $mappingEntityPluralInterface = $namespaceHelper->getHasPluralInterfaceFqnForEntity($mappingEntityClass);
471
            if ($entityReflection->implementsInterface($mappingEntityPluralInterface)) {
472
                $this->assertEquals(
473
                    $mappingEntityClass::getPlural(),
474
                    $mapping['fieldName'],
475
                    sprintf($errorMessage, ' mapping should be plural')
476
                );
477
                $method = 'add'.$mappingEntityClass::getSingular();
478
            } else {
479
                $this->assertEquals(
480
                    $mappingEntityClass::getSingular(),
481
                    $mapping['fieldName'],
482
                    sprintf($errorMessage, ' mapping should be singular')
483
                );
484
                $method = 'set'.$mappingEntityClass::getSingular();
485
            }
486
            $this->assertContains(
487
                strtolower($method),
488
                $methods,
489
                sprintf($errorMessage, $method.' method is not defined')
490
            );
491
            $generated->$method($mappingEntity);
492
        }
493
    }
494
495
    /**
496
     * Get the fully qualified name of the Entity we are testing,
497
     * assumes EntityNameTest as the entity class short name
498
     *
499
     * @return string
500
     */
501
    protected function getTestedEntityFqn(): string
502
    {
503
        if (null === $this->testedEntityFqn) {
504
            $this->testedEntityFqn = \substr(static::class, 0, -4);
505
        }
506
507
        return $this->testedEntityFqn;
508
    }
509
510
    /**
511
     * Get the fully qualified name of the saver for the entity we are testing.
512
     *
513
     * @param EntityInterface $entity
514
     *
515
     * @return string
516
     * @throws \ReflectionException
517
     */
518
    protected function getSaverFqn(
519
        EntityInterface $entity
520
    ): string {
521
        $ref             = new \ReflectionClass($entity);
522
        $entityNamespace = $ref->getNamespaceName();
523
        $saverNamespace  = \str_replace(
524
            'Entities',
525
            'Entity\\Savers',
526
            $entityNamespace
527
        );
528
        $shortName       = $ref->getShortName();
529
530
        return $saverNamespace.'\\'.$shortName.'Saver';
531
    }
532
533
    /**
534
     * Get a \ReflectionClass for the currently tested Entity
535
     *
536
     * @return \ReflectionClass
537
     * @throws \ReflectionException
538
     */
539
    protected function getTestedEntityReflectionClass(): \ReflectionClass
540
    {
541
        if (null === $this->testedEntityReflectionClass) {
542
            $this->testedEntityReflectionClass = new \ReflectionClass(
543
                $this->getTestedEntityFqn()
544
            );
545
        }
546
547
        return $this->testedEntityReflectionClass;
548
    }
549
}
550