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 (#93)
by joseph
71:13 queued 68:42
created

TestEntityGenerator::initFakerGenerator()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
rs 10
c 0
b 0
f 0
ccs 5
cts 5
cp 1
cc 3
nc 3
nop 1
crap 3
1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\DoctrineStaticMeta\Entity\Testing;
4
5
use Doctrine\Common\Collections\ArrayCollection;
6
use Doctrine\ORM\EntityManager;
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\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
    /**
78
     * TestEntityGenerator constructor.
79
     *
80
     * @param float|null                     $seed
81
     * @param array|string[]                 $fakerDataProviderClasses
82
     * @param \ts\Reflection\ReflectionClass $testedEntityReflectionClass
83
     * @param EntitySaverFactory             $entitySaverFactory
84
     * @param EntityValidatorFactory         $entityValidatorFactory
85
     * @SuppressWarnings(PHPMD.StaticAccess)
86
     */
87 18
    public function __construct(
88
        ?float $seed,
89
        array $fakerDataProviderClasses,
90
        \ts\Reflection\ReflectionClass $testedEntityReflectionClass,
91
        EntitySaverFactory $entitySaverFactory,
92
        EntityValidatorFactory $entityValidatorFactory
93
    ) {
94 18
        $this->initFakerGenerator($seed);
95 18
        $this->fakerDataProviderClasses    = $fakerDataProviderClasses;
96 18
        $this->testedEntityReflectionClass = $testedEntityReflectionClass;
97 18
        $this->entitySaverFactory          = $entitySaverFactory;
98 18
        $this->entityValidatorFactory      = $entityValidatorFactory;
99 18
    }
100
101
    /**
102
     * @param float|null $seed
103
     * @SuppressWarnings(PHPMD.StaticAccess)
104
     */
105 18
    protected function initFakerGenerator(?float $seed): void
106
    {
107 18
        if (null === self::$generator) {
108 1
            self::$generator = Faker\Factory::create();
109 1
            if (null !== $seed) {
110 1
                self::$generator->seed($seed);
111
            }
112
        }
113 18
    }
114
115
    /**
116
     * @param EntityManagerInterface   $entityManager
117
     * @param EntityInterface $generated
118
     *
119
     * @throws \Doctrine\ORM\Mapping\MappingException
120
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
121
     * @throws \ErrorException
122
     * @throws \ReflectionException
123
     * @SuppressWarnings(PHPMD.ElseExpression)
124
     */
125 1
    public function addAssociationEntities(
126
        EntityManagerInterface $entityManager,
127
        EntityInterface $generated
128
    ): void {
129 1
        $class    = $this->testedEntityReflectionClass->getName();
130 1
        $meta     = $entityManager->getClassMetadata($class);
131 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

131
        /** @scrutinizer ignore-call */ 
132
        $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...
132 1
        if (empty($mappings)) {
133
            return;
134
        }
135 1
        $namespaceHelper = new NamespaceHelper();
136 1
        $methods         = array_map('strtolower', get_class_methods($generated));
137 1
        foreach ($mappings as $mapping) {
138 1
            $mappingEntityClass = $mapping['targetEntity'];
139 1
            $mappingEntity      = $this->generateEntity($entityManager, $mappingEntityClass);
140 1
            $errorMessage       = "Error adding association entity $mappingEntityClass to $class: %s";
141 1
            $this->entitySaverFactory->getSaverForEntity($mappingEntity)->save($mappingEntity);
142 1
            $mappingEntityPluralInterface = $namespaceHelper->getHasPluralInterfaceFqnForEntity($mappingEntityClass);
143 1
            if (\interface_exists($mappingEntityPluralInterface)
144 1
                && $this->testedEntityReflectionClass->implementsInterface($mappingEntityPluralInterface)
145
            ) {
146 1
                $this->assertSame(
147 1
                    $mappingEntityClass::getPlural(),
148 1
                    $mapping['fieldName'],
149 1
                    sprintf($errorMessage, ' mapping should be plural')
150
                );
151 1
                $getter = 'get' . $mappingEntityClass::getPlural();
152 1
                $method = 'add' . $mappingEntityClass::getSingular();
153
            } else {
154 1
                $this->assertSame(
155 1
                    $mappingEntityClass::getSingular(),
156 1
                    $mapping['fieldName'],
157 1
                    sprintf($errorMessage, ' mapping should be singular')
158
                );
159 1
                $getter = 'get' . $mappingEntityClass::getSingular();
160 1
                $method = 'set' . $mappingEntityClass::getSingular();
161
            }
162 1
            $this->assertInArray(
163 1
                strtolower($method),
164 1
                $methods,
165 1
                sprintf($errorMessage, $method . ' method is not defined')
166
            );
167 1
            $currentlySet = $generated->$getter();
168 1
            switch (true) {
169
                case $currentlySet === null:
170 1
                case $currentlySet === []:
171 1
                case $currentlySet instanceof PersistentCollection:
172 1
                    $generated->$method($mappingEntity);
173 1
                    break;
174
            }
175
        }
176 1
    }
177
178
    /**
179
     * Generate an Entity. Optionally provide an offset from the first entity
180
     *
181
     * @param EntityManagerInterface $entityManager
182
     * @param string                 $class
183
     *
184
     * @param int                    $offset
185
     *
186
     * @return EntityInterface
187
     * @throws \Doctrine\ORM\Mapping\MappingException
188
     * @throws \ReflectionException
189
     * @SuppressWarnings(PHPMD.StaticAccess)
190
     */
191 3
    public function generateEntity(
192
        EntityManagerInterface $entityManager,
193
        string $class,
194
        int $offset = 0
195
    ): EntityInterface {
196
197 3
        $result = $this->generateEntities($entityManager, $class, 1, $offset);
198
199 3
        return current($result);
200
    }
201
202
    /**
203
     * Generate Entities.
204
     *
205
     * Optionally discard the first generated entities up to the value of offset
206
     *
207
     * @param EntityManagerInterface $entityManager
208
     * @param string                 $entityFqn
209
     * @param int                    $num
210
     *
211
     * @param int                    $offset
212
     *
213
     * @return array|EntityInterface[]
214
     * @throws \Doctrine\ORM\Mapping\MappingException
215
     * @throws \ReflectionException
216
     */
217 18
    public function generateEntities(
218
        EntityManagerInterface $entityManager,
219
        string $entityFqn,
220
        int $num,
221
        int $offset = 0
222
    ): array {
223 18
        $this->entityManager = $entityManager;
224 18
        $columnFormatters    = $this->generateColumnFormatters($entityManager, $entityFqn);
225 18
        $meta                = $entityManager->getClassMetadata($entityFqn);
226 18
        $entities            = [];
227 18
        for ($i = 0; $i < ($num + $offset); $i++) {
228 18
            $entity = new $entityFqn($this->entityValidatorFactory);
229 18
            $this->fillColumns($entity, $columnFormatters, $meta);
230 18
            if ($i < $offset) {
231 1
                continue;
232
            }
233 18
            $entities[] = $entity;
234
        }
235 18
        $this->entitySaverFactory->getSaverForEntityFqn($entityFqn)->saveAll($entities);
236
237 18
        return $entities;
238
    }
239
240
    /**
241
     * @param EntityManagerInterface $entityManager
242
     * @param string                 $entityFqn
243
     *
244
     * @return array
245
     * @throws \Doctrine\ORM\Mapping\MappingException
246
     */
247 18
    protected function generateColumnFormatters(EntityManagerInterface $entityManager, string $entityFqn): array
248
    {
249 18
        $meta              = $entityManager->getClassMetadata($entityFqn);
250 18
        $guessedFormatters = (new Faker\ORM\Doctrine\EntityPopulator($meta))->guessColumnFormatters(self::$generator);
251 18
        $customFormatters  = [];
252 18
        $mappings          = $meta->getAssociationMappings();
253 18
        $this->initialiseColumnFormatters($meta, $mappings, $guessedFormatters);
254 18
        $fieldNames = $meta->getFieldNames();
255
256 18
        foreach ($fieldNames as $fieldName) {
257 18
            if (isset($customFormatters[$fieldName])) {
258
                continue;
259
            }
260 18
            if (true === $this->addFakerDataProviderToColumnFormatters($customFormatters, $fieldName, $entityFqn)) {
261
                continue;
262
            }
263 18
            $fieldMapping = $meta->getFieldMapping($fieldName);
264 18
            if (true === ($fieldMapping['unique'] ?? false)) {
265
                $this->addUniqueColumnFormatter($fieldMapping, $customFormatters, $fieldName);
266 18
                continue;
267
            }
268
        }
269
270 18
        return array_merge($guessedFormatters, $customFormatters);
271
    }
272
273
    /**
274
     * Loop through mappings and initialise empty array collections for colection valued mappings, or null if not
275
     *
276
     * @param ClassMetadataInfo $meta
277
     * @param array             $mappings
278
     * @param array             $columnFormatters
279
     */
280 18
    protected function initialiseColumnFormatters(
281
        ClassMetadataInfo $meta,
282
        array &$mappings,
283
        array &$columnFormatters
284
    ): void {
285 18
        foreach ($mappings as $mapping) {
286 2
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
287 2
                $columnFormatters[$mapping['fieldName']] = new ArrayCollection();
288 2
                continue;
289
            }
290
291 2
            if (isset($mapping['joinColumns']) && count($mapping['joinColumns']) === 1
292 2
                && ($mapping['joinColumns'][0]['nullable'] ?? null) === false
293
            ) {
294
                $columnFormatters[$mapping['fieldName']] = function () use ($mapping) {
295
                    $entity = $this->generateEntity($this->entityManager, $mapping['targetEntity']);
296
                    $this->entitySaverFactory->getSaverForEntity($entity)->save($entity);
297
298
                    return $entity;
299
                };
300
                continue;
301
            }
302 2
            $columnFormatters[$mapping['fieldName']] = null;
303
        }
304 18
    }
305
306
    /**
307
     * Add a faker data provider to the columnFormatters array (by reference) if there is one available
308
     *
309
     * Handles instantiating and caching of the data providers
310
     *
311
     * @param array  $columnFormatters
312
     * @param string $fieldName
313
     *
314
     * @param string $entityFqn
315
     *
316
     * @return bool
317
     */
318 18
    protected function addFakerDataProviderToColumnFormatters(
319
        array &$columnFormatters,
320
        string $fieldName,
321
        string $entityFqn
322
    ): bool {
323
        foreach ([
324 18
                     $entityFqn . '-' . $fieldName,
325 18
                     $fieldName,
326
                 ] as $key) {
327 18
            if (!isset($this->fakerDataProviderClasses[$key])) {
328 18
                return false;
329
            }
330
            if (!isset($this->fakerDataProviderObjects[$key])) {
331
                $class                                = $this->fakerDataProviderClasses[$key];
332
                $this->fakerDataProviderObjects[$key] = new $class(self::$generator);
333
            }
334
            $columnFormatters[$fieldName] = $this->fakerDataProviderObjects[$key];
335
336
            return true;
337
        }
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return boolean. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
338
    }
339
340
    protected function addUniqueColumnFormatter(array &$fieldMapping, array &$columnFormatters, string $fieldName): void
341
    {
342
        switch ($fieldMapping['type']) {
343
            case 'string':
344
                $columnFormatters[$fieldName] = $this->getUniqueString();
345
                break;
346
            case 'integer':
347
            case 'bigint':
348
                $columnFormatters[$fieldName] = $this->getUniqueInt();
349
                break;
350
            default:
351
                throw new \InvalidArgumentException('unique field has an unsupported type: '
352
                                                    . print_r($fieldMapping, true));
353
        }
354
    }
355
356
    protected function getUniqueString(): string
357
    {
358
        $string = 'unique string: ' . $this->getUniqueInt() . md5((string)time());
359
        while (isset(self::$uniqueStrings[$string])) {
360
            $string                       = md5((string)time());
361
            self::$uniqueStrings[$string] = true;
362
        }
363
364
        return $string;
365
    }
366
367
    protected function getUniqueInt(): int
368
    {
369
        return ++self::$uniqueInt;
370
    }
371
372 18
    protected function fillColumns(EntityInterface $entity, array &$columnFormatters, ClassMetadata $meta)
373
    {
374 18
        foreach ($columnFormatters as $field => $formatter) {
375 16
            if (null !== $formatter) {
376
                try {
377 16
                    $value = \is_callable($formatter) ? $formatter($entity) : $formatter;
378
                } catch (\InvalidArgumentException $ex) {
379
                    throw new \InvalidArgumentException(
380
                        sprintf(
381
                            'Failed to generate a value for %s::%s: %s',
382
                            \get_class($entity),
383
                            $field,
384
                            $ex->getMessage()
385
                        )
386
                    );
387
                }
388 16
                $meta->reflFields[$field]->setValue($entity, $value);
389
            }
390
        }
391 18
    }
392
393
    /**
394
     * Stub of PHPUnit Assertion method
395
     *
396
     * @param mixed  $expected
397
     * @param mixed  $actual
398
     * @param string $error
399
     *
400
     * @throws \ErrorException
401
     */
402 1
    protected function assertSame($expected, $actual, string $error): void
403
    {
404 1
        if ($expected !== $actual) {
405
            throw new \ErrorException($error);
406
        }
407 1
    }
408
409
    /**
410
     * Stub of PHPUnit Assertion method
411
     *
412
     * @param mixed  $needle
413
     * @param array  $haystack
414
     * @param string $error
415
     *
416
     * @throws \ErrorException
417
     */
418 1
    protected function assertInArray($needle, array $haystack, string $error): void
419
    {
420 1
        if (false === \in_array($needle, $haystack, true)) {
421
            throw new \ErrorException($error);
422
        }
423 1
    }
424
}
425