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.

FakerDataFiller::updateNestedDtosWithFakeData()   B
last analyzed

Complexity

Conditions 10
Paths 10

Size

Total Lines 37
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 24
c 1
b 0
f 0
nc 10
nop 1
dl 0
loc 37
rs 7.6666
ccs 0
cts 33
cp 0
crap 110

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace EdmondsCommerce\DoctrineStaticMeta\Entity\Testing\EntityGenerator;
6
7
use Closure;
8
use Doctrine\Common\Collections\Collection;
9
use Doctrine\DBAL\Types\Type;
10
use Doctrine\ORM\Mapping\MappingException;
11
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\NamespaceHelper;
12
use EdmondsCommerce\DoctrineStaticMeta\DoctrineStaticMeta;
13
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\DataTransferObjectInterface;
14
use EdmondsCommerce\DoctrineStaticMeta\Entity\Testing\EntityGenerator\Faker\ColumnTypeGuesser;
15
use EdmondsCommerce\DoctrineStaticMeta\MappingHelper;
16
use Faker;
17
use Faker\Guesser\Name;
18
use InvalidArgumentException;
19
use ReflectionException;
20
use ReflectionMethod;
21
use RuntimeException;
22
use stdClass;
23
use ts\Reflection\ReflectionClass;
24
25
use function get_class;
26
use function is_callable;
27
28
/**
29
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
30
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
31
 *
32
 */
33
class FakerDataFiller implements FakerDataFillerInterface
34
{
35
    public const DEFAULT_SEED = 688377.0;
36
    /**
37
     * @var Faker\Generator
38
     */
39
    private static $generator;
40
    /**
41
     * These two are used to keep track of unique fields and ensure we dont accidently make apply none unique values
42
     *
43
     * @var array
44
     */
45
    private static $uniqueStrings = [];
46
    /**
47
     * @var int
48
     */
49
    private static $uniqueInt;
50
    /**
51
     * @var array
52
     */
53
    private static $processedDtos = [];
54
    /**
55
     * An array of fieldNames to class names that are to be instantiated as column formatters as required
56
     *
57
     * @var array|string[]
58
     */
59
    private $fakerDataProviderClasses;
60
    /**
61
     * A cache of instantiated column data providers
62
     *
63
     * @var array
64
     */
65
    private $fakerDataProviderObjects = [];
66
    /**
67
     * @var array
68
     */
69
    private $columnFormatters;
70
    /**
71
     * @var DoctrineStaticMeta
72
     */
73
    private $testedEntityDsm;
74
    /**
75
     * @var Name
76
     */
77
    private $nameGuesser;
78
    /**
79
     * @var ColumnTypeGuesser
80
     */
81
    private $columnTypeGuesser;
82
    /**
83
     * @var NamespaceHelper
84
     */
85
    private $namespaceHelper;
86
    /**
87
     * @var FakerDataFillerFactory
88
     */
89
    private $fakerDataFillerFactory;
90
91
    public function __construct(
92
        FakerDataFillerFactory $fakerDataFillerFactory,
93
        DoctrineStaticMeta $testedEntityDsm,
94
        NamespaceHelper $namespaceHelper,
95
        array $fakerDataProviderClasses,
96
        ?float $seed = null
97
    ) {
98
        $this->initFakerGenerator($seed);
99
        $this->testedEntityDsm          = $testedEntityDsm;
100
        $this->fakerDataProviderClasses = $fakerDataProviderClasses;
101
        $this->nameGuesser              = new Name(self::$generator);
102
        $this->columnTypeGuesser        = new ColumnTypeGuesser(self::$generator);
103
        $this->namespaceHelper          = $namespaceHelper;
104
        $this->checkFakerClassesRootNamespaceMatchesEntityFqn(
105
            $this->testedEntityDsm->getReflectionClass()->getName()
106
        );
107
        $this->generateColumnFormatters();
108
        $this->fakerDataFillerFactory = $fakerDataFillerFactory;
109
    }
110
111
112
    /**
113
     * @param float|null $seed
114
     * @SuppressWarnings(PHPMD.StaticAccess)
115
     */
116
    private function initFakerGenerator(?float $seed): void
117
    {
118
        if (null === self::$generator) {
119
            self::$generator = Faker\Factory::create();
120
        }
121
        self::$generator->seed($seed ?? self::DEFAULT_SEED);
122
    }
123
124
    private function checkFakerClassesRootNamespaceMatchesEntityFqn(string $fakedEntityFqn): void
125
    {
126
        if ([] === $this->fakerDataProviderClasses) {
127
            return;
128
        }
129
        $projectRootNamespace = null;
130
        foreach (array_keys($this->fakerDataProviderClasses) as $classField) {
131
            if (false === \ts\stringContains($classField, '-')) {
132
                continue;
133
            }
134
            [$entityFqn,] = explode('-', $classField);
135
            $rootNamespace = $this->namespaceHelper->getProjectNamespaceRootFromEntityFqn($entityFqn);
136
            if (null === $projectRootNamespace) {
137
                $projectRootNamespace = $rootNamespace;
138
                continue;
139
            }
140
            if ($rootNamespace !== $projectRootNamespace) {
141
                throw new RuntimeException(
142
                    'Found unexpected root namespace ' .
143
                    $rootNamespace .
144
                    ', expecting ' .
145
                    $projectRootNamespace .
0 ignored issues
show
Bug introduced by
Are you sure $projectRootNamespace of type void can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

145
                    /** @scrutinizer ignore-type */ $projectRootNamespace .
Loading history...
146
                    ', do we have mixed fakerProviderClasses? ' .
147
                    print_r($this->fakerDataProviderClasses, true)
148
                );
149
            }
150
        }
151
        if (null === $projectRootNamespace) {
152
            return;
153
        }
154
        $fakedEntityRootNamespace = $this->namespaceHelper->getProjectNamespaceRootFromEntityFqn($fakedEntityFqn);
155
        if ($fakedEntityRootNamespace === $projectRootNamespace) {
156
            return;
157
        }
158
        throw new RuntimeException('Faked entity FQN ' .
159
                                   $fakedEntityFqn .
160
                                   ' project root namespace does not match the faker classes root namespace ' .
161
                                   $projectRootNamespace);
162
    }
163
164
    /**
165
     * @throws MappingException
166
     */
167
    private function generateColumnFormatters(): void
168
    {
169
        $entityFqn  = $this->testedEntityDsm->getReflectionClass()->getName();
170
        $meta       = $this->testedEntityDsm->getMetaData();
171
        $fieldNames = $meta->getFieldNames();
172
        foreach ($fieldNames as $fieldName) {
173
            if (isset($this->columnFormatters[$fieldName])) {
174
                continue;
175
            }
176
            if (true === $this->addFakerDataProviderToColumnFormatters($fieldName, $entityFqn)) {
177
                continue;
178
            }
179
            $fieldMapping = $meta->getFieldMapping($fieldName);
180
            if (true === ($fieldMapping['unique'] ?? false)) {
181
                $this->addUniqueColumnFormatter($fieldMapping, $fieldName);
182
                continue;
183
            }
184
        }
185
        $this->guessMissingColumnFormatters();
186
    }
187
188
    /**
189
     * Add a faker data provider to the columnFormatters array (by reference) if there is one available
190
     *
191
     * Handles instantiating and caching of the data providers
192
     *
193
     * @param string $fieldName
194
     *
195
     * @param string $entityFqn
196
     *
197
     * @return bool
198
     */
199
    private function addFakerDataProviderToColumnFormatters(
200
        string $fieldName,
201
        string $entityFqn
202
    ): bool {
203
        foreach (
204
            [
205
                     $entityFqn . '-' . $fieldName,
206
                     $fieldName,
207
                 ] as $key
208
        ) {
209
            if (!isset($this->fakerDataProviderClasses[$key])) {
210
                continue;
211
            }
212
            if (!isset($this->fakerDataProviderObjects[$key])) {
213
                $class                                = $this->fakerDataProviderClasses[$key];
214
                $this->fakerDataProviderObjects[$key] = new $class(self::$generator);
215
            }
216
            $this->columnFormatters[$fieldName] = $this->fakerDataProviderObjects[$key];
217
218
            return true;
219
        }
220
221
        return false;
222
    }
223
224
    private function addUniqueColumnFormatter(array &$fieldMapping, string $fieldName): void
225
    {
226
        switch ($fieldMapping['type']) {
227
            case MappingHelper::TYPE_UUID:
228
            case MappingHelper::TYPE_NON_ORDERED_BINARY_UUID:
229
            case MappingHelper::TYPE_NON_BINARY_UUID:
230
                return;
231
            case MappingHelper::TYPE_STRING:
232
                $this->columnFormatters[$fieldName] = function () {
233
                    return $this->getUniqueString();
234
                };
235
                break;
236
            case MappingHelper::TYPE_INTEGER:
237
            case Type::BIGINT:
238
                $this->columnFormatters[$fieldName] = function () {
239
                    return $this->getUniqueInt();
240
                };
241
                break;
242
            default:
243
                throw new InvalidArgumentException('unique field has an unsupported type: '
244
                                                   . print_r($fieldMapping, true));
245
        }
246
    }
247
248
    private function getUniqueString(): string
249
    {
250
        $string = 'unique string: ' . $this->getUniqueInt() . md5((string)time());
251
        while (isset(self::$uniqueStrings[$string])) {
252
            $string                       = md5((string)time());
253
            self::$uniqueStrings[$string] = true;
254
        }
255
256
        return $string;
257
    }
258
259
    private function getUniqueInt(): int
260
    {
261
        return ++self::$uniqueInt;
262
    }
263
264
    /**
265
     * @SuppressWarnings(PHPMD) - it can't seem to handle this method
266
     */
267
    private function guessMissingColumnFormatters(): void
268
    {
269
        $meta = $this->testedEntityDsm->getMetaData();
270
        foreach ($meta->getFieldNames() as $fieldName) {
271
            if (
272
                isset($this->columnFormatters[$fieldName])
273
                || $meta->isIdentifier($fieldName)
274
                || !$meta->hasField($fieldName)
275
                || false !== \ts\stringContains($fieldName, '.')
276
                || null === $this->testedEntityDsm->getSetterNameFromPropertyName($fieldName)
277
            ) {
278
                continue;
279
            }
280
            $size = $meta->fieldMappings[$fieldName]['length'] ?? null;
281
            if (null !== $formatter = $this->guessByName($fieldName, $size)) {
282
                $this->columnFormatters[$fieldName] = $formatter;
283
                continue;
284
            }
285
            if (null !== $formatter = $this->columnTypeGuesser->guessFormat($fieldName, $meta)) {
286
                $this->columnFormatters[$fieldName] = $formatter;
287
                continue;
288
            }
289
            if (MappingHelper::TYPE_ARRAY === $meta->fieldMappings[$fieldName]['type']) {
290
                $this->columnFormatters[$fieldName] = $this->getArray();
291
            }
292
            if (MappingHelper::TYPE_OBJECT === $meta->fieldMappings[$fieldName]['type']) {
293
                $this->columnFormatters[$fieldName] = $this->getObject();
294
            }
295
        }
296
    }
297
298
    private function guessByName(string $fieldName, ?int $size): ?Closure
299
    {
300
        $formatter = $this->nameGuesser->guessFormat($fieldName, $size);
301
        if (null !== $formatter) {
0 ignored issues
show
introduced by
The condition null !== $formatter is always true.
Loading history...
302
            return $formatter;
303
        }
304
        if (false !== \ts\stringContains($fieldName, 'email')) {
305
            return static function () {
306
                return self::$generator->email;
307
            };
308
        }
309
310
        return null;
311
    }
312
313
    /**
314
     * Json should not be a string, it should be data that is then encoded to Json by the Json Type
315
     *
316
     * @return callable
317
     * @see \Doctrine\DBAL\Types\JsonType::convertToDatabaseValue
318
     */
319
    private function getArray(): callable
320
    {
321
        return static function () {
322
            $toEncode                     = [];
323
            $toEncode['string']           = self::$generator->text;
324
            $toEncode['float']            = self::$generator->randomFloat();
325
            $toEncode['nested']['string'] = self::$generator->text;
326
            $toEncode['nested']['float']  = self::$generator->randomFloat();
327
328
            return $toEncode;
329
        };
330
    }
331
332
    private function getObject(): callable
333
    {
334
        return static function () {
335
            $toEncode                 = new stdClass();
336
            $toEncode->string         = self::$generator->text;
337
            $toEncode->float          = self::$generator->randomFloat();
338
            $toEncode->nested->string = self::$generator->text;
339
            $toEncode->nested->float  = self::$generator->randomFloat();
340
341
            return $toEncode;
342
        };
343
    }
344
345
    public function updateDtoWithFakeData(DataTransferObjectInterface $dto): void
346
    {
347
        $this->update($dto, true);
348
    }
349
350
    /**
351
     * @param DataTransferObjectInterface $dto
352
     * @param bool                        $isRootDto
353
     *
354
     * @throws ReflectionException
355
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
356
     */
357
    public function update(DataTransferObjectInterface $dto, $isRootDto = false): void
358
    {
359
        if (true === $isRootDto) {
360
            self::$processedDtos = [];
361
        }
362
        if (true === $this->processed($dto)) {
363
            return;
364
        }
365
366
        $dtoHash                       = spl_object_hash($dto);
367
        self::$processedDtos[$dtoHash] = true;
368
        $this->updateFieldsWithFakeData($dto);
369
        $this->updateNestedDtosWithFakeData($dto);
370
    }
371
372
    private function processed(DataTransferObjectInterface $dto): bool
373
    {
374
        return array_key_exists(spl_object_hash($dto), self::$processedDtos);
375
    }
376
377
    private function updateFieldsWithFakeData(DataTransferObjectInterface $dto): void
378
    {
379
        if (null === $this->columnFormatters) {
380
            return;
381
        }
382
        foreach ($this->columnFormatters as $field => $formatter) {
383
            if (null === $formatter) {
384
                continue;
385
            }
386
            try {
387
                $value  = is_callable($formatter) ? $formatter($dto) : $formatter;
388
                $setter = 'set' . $field;
389
                $dto->$setter($value);
390
            } catch (InvalidArgumentException $ex) {
391
                throw new InvalidArgumentException(
392
                    sprintf(
393
                        'Failed to generate a value for %s::%s: %s',
394
                        get_class($dto),
395
                        $field,
396
                        $ex->getMessage()
397
                    )
398
                );
399
            }
400
        }
401
    }
402
403
    /**
404
     * @param DataTransferObjectInterface $dto
405
     *
406
     * @throws ReflectionException
407
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
408
     */
409
    private function updateNestedDtosWithFakeData(DataTransferObjectInterface $dto): void
410
    {
411
        $reflection = new ReflectionClass(get_class($dto));
412
413
        $reflectionMethods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
414
        foreach ($reflectionMethods as $reflectionMethod) {
415
            $reflectionMethodReturnType = $reflectionMethod->getReturnType();
416
            if (null === $reflectionMethodReturnType) {
417
                continue;
418
            }
419
            $returnTypeName = $reflectionMethodReturnType->getName();
420
            $methodName     = $reflectionMethod->getName();
421
            if (false === \ts\stringStartsWith($methodName, 'get')) {
422
                continue;
423
            }
424
            if (substr($returnTypeName, -3) === 'Dto') {
425
                $isDtoMethod = 'isset' . substr($methodName, 3, -3) . 'AsDto';
426
                if (false === $dto->$isDtoMethod()) {
427
                    continue;
428
                }
429
                $got = $dto->$methodName();
430
                if ($got instanceof DataTransferObjectInterface) {
431
                    $this->updateNestedDtoUsingNewFakerFiller($got);
432
                }
433
                continue;
434
            }
435
            if ($returnTypeName === Collection::class) {
436
                /**
437
                 * @var Collection
438
                 */
439
                $collection = $dto->$methodName();
440
                foreach ($collection as $got) {
441
                    if ($got instanceof DataTransferObjectInterface) {
442
                        $this->updateNestedDtoUsingNewFakerFiller($got);
443
                    }
444
                }
445
                continue;
446
            }
447
        }
448
    }
449
450
    /**
451
     * Get an instance of the Faker filler for this DTO, but do not regard it as root
452
     *
453
     * @param DataTransferObjectInterface $dto
454
     */
455
    private function updateNestedDtoUsingNewFakerFiller(DataTransferObjectInterface $dto): void
456
    {
457
        $dtoFqn = get_class($dto);
458
        $this->fakerDataFillerFactory
459
            ->getInstanceFromDataTransferObjectFqn($dtoFqn)
460
            ->update($dto);
461
    }
462
}
463