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 (#197)
by joseph
21:07
created

FakerDataFiller   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 404
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 189
dl 0
loc 404
rs 3.12
c 0
b 0
f 0
ccs 0
cts 278
cp 0
wmc 66

17 Methods

Rating   Name   Duplication   Size   Complexity  
A updateDtoWithFakeData() 0 3 1
A updateFieldsWithFakeData() 0 20 6
A getJson() 0 9 1
A update() 0 13 3
A processed() 0 3 1
A guessByName() 0 13 3
A updateNestedDtoUsingNewFakerFiller() 0 6 1
B updateNestedDtosWithFakeData() 0 37 10
A __construct() 0 18 1
B addUniqueColumnFormatter() 0 21 7
A initFakerGenerator() 0 6 2
A addFakerDataProviderToColumnFormatters() 0 21 4
B guessMissingColumnFormatters() 0 29 10
A getUniqueInt() 0 3 1
A getUniqueString() 0 9 2
A generateColumnFormatters() 0 19 5
B checkFakerClassesRootNamespaceMatchesEntityFqn() 0 38 8

How to fix   Complexity   

Complex Class

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

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