GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#137)
by joseph
60:53 queued 35:20
created

TestEntityGenerator   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 468
Duplicated Lines 0 %

Test Coverage

Coverage 77.58%

Importance

Changes 0
Metric Value
eloc 162
dl 0
loc 468
ccs 128
cts 165
cp 0.7758
rs 7.44
c 0
b 0
f 0
wmc 52

17 Methods

Rating   Name   Duplication   Size   Complexity  
A generateEntities() 0 12 1
A generateColumnFormatters() 0 24 5
A getUniqueInt() 0 3 1
A fillColumns() 0 17 5
A create() 0 5 1
A addFakerDataProviderToColumnFormatters() 0 22 4
A getUniqueString() 0 9 2
A assertInArray() 0 4 2
A assertSame() 0 4 2
A __construct() 0 16 1
A addUniqueColumnFormatter() 0 15 5
A initialiseColumnFormatters() 0 23 6
A generateUnsavedEntities() 0 19 3
A initFakerGenerator() 0 6 3
B addAssociationEntities() 0 49 8
A getGenerator() 0 9 2
A generateEntity() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like TestEntityGenerator 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 TestEntityGenerator, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\DoctrineStaticMeta\Entity\Testing\EntityGenerator;
4
5
use Doctrine\Common\Collections\ArrayCollection;
6
use Doctrine\DBAL\Types\Type;
7
use Doctrine\ORM\EntityManagerInterface;
8
use Doctrine\ORM\Mapping\ClassMetadata;
9
use Doctrine\ORM\Mapping\ClassMetadataInfo;
10
use Doctrine\ORM\PersistentCollection;
11
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\NamespaceHelper;
12
use EdmondsCommerce\DoctrineStaticMeta\Entity\Factory\EntityFactory;
13
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\EntityInterface;
14
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\EntitySaverFactory;
15
use EdmondsCommerce\DoctrineStaticMeta\Entity\Validation\EntityValidatorFactory;
16
use EdmondsCommerce\DoctrineStaticMeta\MappingHelper;
17
use Faker;
18
19
/**
20
 * Class TestEntityGenerator
21
 *
22
 * This class handles utilising Faker to build up an Entity and then also possible build associated entities and handle
23
 * the association
24
 *
25
 * Unique columns are guaranteed to have a totally unique value in this particular process, but not between processes
26
 *
27
 * This Class provides you a few ways to generate test Entities, either in bulk or one at a time
28
 *ExcessiveClassComplexity
29
 * @package EdmondsCommerce\DoctrineStaticMeta\Entity\Testing
30
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
31
 *          @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
32
 */
33
class TestEntityGenerator
34
{
35
    /**
36
     * @var Faker\Generator
37
     */
38
    protected static $generator;
39
    /**
40
     * These two are used to keep track of unique fields and ensure we dont accidently make apply none unique values
41
     *
42
     * @var array
43
     */
44
    private static $uniqueStrings = [];
45
    /**
46
     * @var int
47
     */
48
    private static $uniqueInt;
49
    /**
50
     * @var EntityManagerInterface
51
     */
52
    protected $entityManager;
53
    /**
54
     * An array of fieldNames to class names that are to be instantiated as column formatters as required
55
     *
56
     * @var array|string[]
57
     */
58
    protected $fakerDataProviderClasses;
59
60
    /**
61
     * A cache of instantiated column data providers
62
     *
63
     * @var array
64
     */
65
    protected $fakerDataProviderObjects = [];
66
67
    /**
68
     * Reflection of the tested entity
69
     *
70
     * @var \ts\Reflection\ReflectionClass
71
     */
72
    protected $testedEntityReflectionClass;
73
    /**
74
     * @var EntitySaverFactory
75
     */
76
    protected $entitySaverFactory;
77
    /**
78
     * @var EntityValidatorFactory
79
     */
80
    protected $entityValidatorFactory;
81
    /**
82
     * @var EntityFactory
83
     */
84
    protected $entityFactory;
85
86
    /**
87
     * TestEntityGenerator constructor.
88
     *
89
     * @param array|string[]                 $fakerDataProviderClasses
90
     * @param \ts\Reflection\ReflectionClass $testedEntityReflectionClass
91
     * @param EntitySaverFactory             $entitySaverFactory
92
     * @param EntityValidatorFactory         $entityValidatorFactory
93
     * @param float|null                     $seed
94
     * @param EntityFactory|null             $entityFactory
95
     * @SuppressWarnings(PHPMD.StaticAccess)
96
     */
97 3
    public function __construct(
98
        array $fakerDataProviderClasses,
99
        \ts\Reflection\ReflectionClass $testedEntityReflectionClass,
100
        EntitySaverFactory $entitySaverFactory,
101
        EntityValidatorFactory $entityValidatorFactory,
102
        ?float $seed = null,
103
        ?EntityFactory $entityFactory = null
104
    ) {
105 3
        $this->initFakerGenerator($seed);
106 3
        $this->fakerDataProviderClasses    = $fakerDataProviderClasses;
107 3
        $this->testedEntityReflectionClass = $testedEntityReflectionClass;
108 3
        $this->entitySaverFactory          = $entitySaverFactory;
109 3
        $this->entityValidatorFactory      = $entityValidatorFactory;
110 3
        $this->entityFactory               = $entityFactory ?? new EntityFactory(
111 3
            $entityValidatorFactory,
112 3
            new NamespaceHelper()
113
        );
114 3
    }
115
116
    /**
117
     * @param float|null $seed
118
     * @SuppressWarnings(PHPMD.StaticAccess)
119
     */
120 3
    protected function initFakerGenerator(?float $seed): void
121
    {
122 3
        if (null === self::$generator) {
123 1
            self::$generator = Faker\Factory::create();
124 1
            if (null !== $seed) {
125 1
                self::$generator->seed($seed);
126
            }
127
        }
128 3
    }
129
130
    /**
131
     * Use the factory to generate a new Entity, possibly with values set as well
132
     *
133
     * @param EntityManagerInterface $entityManager
134
     * @param array                  $values
135
     *
136
     * @return EntityInterface
137
     */
138 2
    public function create(EntityManagerInterface $entityManager, array $values = []): EntityInterface
139
    {
140 2
        $this->entityFactory->setEntityManager($entityManager);
141
142 2
        return $this->entityFactory->create($this->testedEntityReflectionClass->getName(), $values);
143
    }
144
145
    /**
146
     * @param EntityManagerInterface $entityManager
147
     * @param EntityInterface        $generated
148
     *
149
     * @throws \Doctrine\ORM\Mapping\MappingException
150
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
151
     * @throws \ErrorException
152
     * @throws \ReflectionException
153
     * @SuppressWarnings(PHPMD.ElseExpression)
154
     */
155 1
    public function addAssociationEntities(
156
        EntityManagerInterface $entityManager,
157
        EntityInterface $generated
158
    ): void {
159 1
        $class    = $this->testedEntityReflectionClass->getName();
160 1
        $meta     = $entityManager->getClassMetadata($class);
161 1
        $mappings = $meta->getAssociationMappings();
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

161
        /** @scrutinizer ignore-call */ 
162
        $mappings = $meta->getAssociationMappings();

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...
162 1
        if (empty($mappings)) {
163
            return;
164
        }
165 1
        $namespaceHelper = new NamespaceHelper();
166 1
        $methods         = array_map('strtolower', get_class_methods($generated));
167 1
        foreach ($mappings as $mapping) {
168 1
            $mappingEntityClass = $mapping['targetEntity'];
169 1
            $mappingEntity      = $this->generateEntity($entityManager, $mappingEntityClass);
170 1
            $errorMessage       = "Error adding association entity $mappingEntityClass to $class: %s";
171 1
            $this->entitySaverFactory->getSaverForEntity($mappingEntity)->save($mappingEntity);
172 1
            $mappingEntityPluralInterface = $namespaceHelper->getHasPluralInterfaceFqnForEntity($mappingEntityClass);
173 1
            if (\interface_exists($mappingEntityPluralInterface)
174 1
                && $this->testedEntityReflectionClass->implementsInterface($mappingEntityPluralInterface)
175
            ) {
176 1
                $this->assertSame(
177 1
                    $mappingEntityClass::getDoctrineStaticMeta()->getPlural(),
178 1
                    $mapping['fieldName'],
179 1
                    sprintf($errorMessage, ' mapping should be plural')
180
                );
181 1
                $getter = 'get' . $mappingEntityClass::getDoctrineStaticMeta()->getPlural();
182 1
                $method = 'add' . $mappingEntityClass::getDoctrineStaticMeta()->getSingular();
183
            } else {
184 1
                $this->assertSame(
185 1
                    $mappingEntityClass::getDoctrineStaticMeta()->getSingular(),
186 1
                    $mapping['fieldName'],
187 1
                    sprintf($errorMessage, ' mapping should be singular')
188
                );
189 1
                $getter = 'get' . $mappingEntityClass::getDoctrineStaticMeta()->getSingular();
190 1
                $method = 'set' . $mappingEntityClass::getDoctrineStaticMeta()->getSingular();
191
            }
192 1
            $this->assertInArray(
193 1
                strtolower($method),
194 1
                $methods,
195 1
                sprintf($errorMessage, $method . ' method is not defined')
196
            );
197 1
            $currentlySet = $generated->$getter();
198 1
            switch (true) {
199
                case $currentlySet === null:
200 1
                case $currentlySet === []:
201 1
                case $currentlySet instanceof PersistentCollection:
202 1
                    $generated->$method($mappingEntity);
203 1
                    break;
204
            }
205
        }
206 1
    }
207
208
    /**
209
     * Generate an Entity. Optionally provide an offset from the first entity
210
     *
211
     * @param EntityManagerInterface $entityManager
212
     * @param string                 $class
213
     *
214
     * @param int                    $offset
215
     *
216
     * @return EntityInterface
217
     * @throws \Doctrine\ORM\Mapping\MappingException
218
     * @throws \ReflectionException
219
     * @SuppressWarnings(PHPMD.StaticAccess)
220
     */
221 3
    public function generateEntity(
222
        EntityManagerInterface $entityManager,
223
        string $class,
224
        int $offset = 0
225
    ): EntityInterface {
226
227 3
        $result = $this->generateEntities($entityManager, $class, 1, $offset);
228
229 3
        return current($result);
230
    }
231
232
    /**
233
     * Generate Entities.
234
     *
235
     * Optionally discard the first generated entities up to the value of offset
236
     *
237
     * @param EntityManagerInterface $entityManager
238
     * @param string                 $entityFqn
239
     * @param int                    $num
240
     *
241
     * @param int                    $offset
242
     *
243
     * @return array|EntityInterface[]
244
     * @throws \Doctrine\ORM\Mapping\MappingException
245
     * @throws \ReflectionException
246
     */
247 4
    public function generateEntities(
248
        EntityManagerInterface $entityManager,
249
        string $entityFqn,
250
        int $num,
251
        int $offset = 0
252
    ): array {
253
254 4
        $entities = $this->generateUnsavedEntities($entityManager, $entityFqn, $num, $offset);
255 4
        $this->entitySaverFactory->getSaverForEntityFqn($entityFqn)
256 4
                                 ->saveAll($entities);
257
258 4
        return $entities;
259
    }
260
261
    /**
262
     * Generate Entities but do not save them
263
     *
264
     * @param EntityManagerInterface $entityManager
265
     * @param string                 $entityFqn
266
     * @param int                    $num
267
     * @param int                    $offset
268
     *
269
     * @return array
270
     * @throws \Doctrine\ORM\Mapping\MappingException
271
     */
272 3
    public function generateUnsavedEntities(
273
        EntityManagerInterface $entityManager,
274
        string $entityFqn,
275
        int $num,
276
        int $offset = 0
277
    ): array {
278 3
        $this->entityManager = $entityManager;
279 3
        $entities            = [];
280 3
        $generator           = $this->getGenerator($entityManager, $entityFqn);
281 3
        for ($i = 0; $i < ($num + $offset); $i++) {
282 3
            $generator->next();
283 3
            $entity = $generator->current();
284 3
            if ($i < $offset) {
285 1
                continue;
286
            }
287 3
            $entities[] = $entity;
288
        }
289
290 3
        return $entities;
291
    }
292
293
    /**
294
     * Get an instance of \Generator which can then be used in foreach loops or manually to provide a continuous stream
295
     * of generated Entities
296
     *
297
     * @param EntityManagerInterface $entityManager
298
     * @param string                 $entityFqn
299
     *
300
     * @return \Generator
301
     * @throws \Doctrine\ORM\Mapping\MappingException
302
     */
303 3
    public function getGenerator(EntityManagerInterface $entityManager, string $entityFqn): \Generator
304
    {
305 3
        $this->entityManager = $entityManager;
306 3
        $columnFormatters    = $this->generateColumnFormatters($entityManager, $entityFqn);
307 3
        $meta                = $entityManager->getClassMetadata($entityFqn);
308 3
        while (true) {
309 3
            $entity = new $entityFqn($this->entityValidatorFactory);
310 3
            $this->fillColumns($entity, $columnFormatters, $meta);
311 3
            yield $entity;
312
        }
313
    }
314
315
    /**
316
     * @param EntityManagerInterface $entityManager
317
     * @param string                 $entityFqn
318
     *
319
     * @return array
320
     * @throws \Doctrine\ORM\Mapping\MappingException
321
     */
322 3
    protected function generateColumnFormatters(EntityManagerInterface $entityManager, string $entityFqn): array
323
    {
324 3
        $meta              = $entityManager->getClassMetadata($entityFqn);
325 3
        $guessedFormatters = (new Faker\ORM\Doctrine\EntityPopulator($meta))->guessColumnFormatters(self::$generator);
326 3
        $customFormatters  = [];
327 3
        $mappings          = $meta->getAssociationMappings();
328 3
        $this->initialiseColumnFormatters($meta, $mappings, $guessedFormatters);
329 3
        $fieldNames = $meta->getFieldNames();
330
331 3
        foreach ($fieldNames as $fieldName) {
332 3
            if (isset($customFormatters[$fieldName])) {
333
                continue;
334
            }
335 3
            if (true === $this->addFakerDataProviderToColumnFormatters($customFormatters, $fieldName, $entityFqn)) {
336
                continue;
337
            }
338 3
            $fieldMapping = $meta->getFieldMapping($fieldName);
339 3
            if (true === ($fieldMapping['unique'] ?? false)) {
340 3
                $this->addUniqueColumnFormatter($fieldMapping, $customFormatters, $fieldName);
341 3
                continue;
342
            }
343
        }
344
345 3
        return array_merge($guessedFormatters, $customFormatters);
346
    }
347
348
    /**
349
     * Loop through mappings and initialise empty array collections for colection valued mappings, or null if not
350
     *
351
     * @param ClassMetadataInfo $meta
352
     * @param array             $mappings
353
     * @param array             $columnFormatters
354
     */
355 3
    protected function initialiseColumnFormatters(
356
        ClassMetadataInfo $meta,
357
        array &$mappings,
358
        array &$columnFormatters
359
    ): void {
360 3
        foreach ($mappings as $mapping) {
361 3
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
362 3
                $columnFormatters[$mapping['fieldName']] = new ArrayCollection();
363 3
                continue;
364
            }
365
366 3
            if (isset($mapping['joinColumns']) && count($mapping['joinColumns']) === 1
367 3
                && ($mapping['joinColumns'][0]['nullable'] ?? null) === false
368
            ) {
369
                $columnFormatters[$mapping['fieldName']] = function () use ($mapping) {
370
                    $entity = $this->generateEntity($this->entityManager, $mapping['targetEntity']);
371
                    $this->entitySaverFactory->getSaverForEntity($entity)->save($entity);
372
373
                    return $entity;
374
                };
375
                continue;
376
            }
377 3
            $columnFormatters[$mapping['fieldName']] = null;
378
        }
379 3
    }
380
381
    /**
382
     * Add a faker data provider to the columnFormatters array (by reference) if there is one available
383
     *
384
     * Handles instantiating and caching of the data providers
385
     *
386
     * @param array  $columnFormatters
387
     * @param string $fieldName
388
     *
389
     * @param string $entityFqn
390
     *
391
     * @return bool
392
     */
393 3
    protected function addFakerDataProviderToColumnFormatters(
394
        array &$columnFormatters,
395
        string $fieldName,
396
        string $entityFqn
397
    ): bool {
398
        foreach ([
399 3
                     $entityFqn . '-' . $fieldName,
400 3
                     $fieldName,
401
                 ] as $key) {
402 3
            if (!isset($this->fakerDataProviderClasses[$key])) {
403 3
                continue;
404
            }
405
            if (!isset($this->fakerDataProviderObjects[$key])) {
406
                $class                                = $this->fakerDataProviderClasses[$key];
407
                $this->fakerDataProviderObjects[$key] = new $class(self::$generator);
408
            }
409
            $columnFormatters[$fieldName] = $this->fakerDataProviderObjects[$key];
410
411
            return true;
412
        }
413
414 3
        return false;
415
    }
416
417 3
    protected function addUniqueColumnFormatter(array &$fieldMapping, array &$columnFormatters, string $fieldName): void
418
    {
419 3
        switch ($fieldMapping['type']) {
420
            case MappingHelper::TYPE_UUID:
421 3
                return;
422
            case MappingHelper::TYPE_STRING:
423
                $columnFormatters[$fieldName] = $this->getUniqueString();
424
                break;
425
            case MappingHelper::TYPE_INTEGER:
426
            case Type::BIGINT:
427
                $columnFormatters[$fieldName] = $this->getUniqueInt();
428
                break;
429
            default:
430
                throw new \InvalidArgumentException('unique field has an unsupported type: '
431
                                                    . print_r($fieldMapping, true));
432
        }
433
    }
434
435
    protected function getUniqueString(): string
436
    {
437
        $string = 'unique string: ' . $this->getUniqueInt() . md5((string)time());
438
        while (isset(self::$uniqueStrings[$string])) {
439
            $string                       = md5((string)time());
440
            self::$uniqueStrings[$string] = true;
441
        }
442
443
        return $string;
444
    }
445
446
    protected function getUniqueInt(): int
447
    {
448
        return ++self::$uniqueInt;
449
    }
450
451 3
    protected function fillColumns(EntityInterface $entity, array &$columnFormatters, ClassMetadata $meta): void
452
    {
453 3
        foreach ($columnFormatters as $field => $formatter) {
454 3
            if (null !== $formatter) {
455
                try {
456 3
                    $value = \is_callable($formatter) ? $formatter($entity) : $formatter;
457
                } catch (\InvalidArgumentException $ex) {
458
                    throw new \InvalidArgumentException(
459
                        sprintf(
460
                            'Failed to generate a value for %s::%s: %s',
461
                            \get_class($entity),
462
                            $field,
463
                            $ex->getMessage()
464
                        )
465
                    );
466
                }
467 3
                $meta->reflFields[$field]->setValue($entity, $value);
468
            }
469
        }
470 3
    }
471
472
    /**
473
     * Stub of PHPUnit Assertion method
474
     *
475
     * @param mixed  $expected
476
     * @param mixed  $actual
477
     * @param string $error
478
     *
479
     * @throws \ErrorException
480
     */
481 1
    protected function assertSame($expected, $actual, string $error): void
482
    {
483 1
        if ($expected !== $actual) {
484
            throw new \ErrorException($error);
485
        }
486 1
    }
487
488
    /**
489
     * Stub of PHPUnit Assertion method
490
     *
491
     * @param mixed  $needle
492
     * @param array  $haystack
493
     * @param string $error
494
     *
495
     * @throws \ErrorException
496
     */
497 1
    protected function assertInArray($needle, array $haystack, string $error): void
498
    {
499 1
        if (false === \in_array($needle, $haystack, true)) {
500
            throw new \ErrorException($error);
501
        }
502 1
    }
503
}
504