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
Push — master ( d68f0e...658fac )
by Ross
14s queued 12s
created

FakerDataFiller   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 399
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 187
dl 0
loc 399
ccs 0
cts 275
cp 0
rs 3.2
c 0
b 0
f 0
wmc 65

17 Methods

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

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

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