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 (#126)
by joseph
28:55
created

TestEntityGenerator   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 421
Duplicated Lines 0 %

Test Coverage

Coverage 72.9%

Importance

Changes 0
Metric Value
wmc 48
eloc 151
dl 0
loc 421
ccs 113
cts 155
cp 0.729
rs 8.5599
c 0
b 0
f 0

15 Methods

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

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