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 ( 84e592...894b2c )
by joseph
127:56 queued 125:09
created

FakerDataFiller   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 399
Duplicated Lines 0 %

Test Coverage

Coverage 49.82%

Importance

Changes 0
Metric Value
eloc 187
dl 0
loc 399
ccs 137
cts 275
cp 0.4982
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 getUniqueInt() 0 3 1
A getUniqueString() 0 9 2
A updateDtoWithFakeData() 0 3 1
A updateFieldsWithFakeData() 0 20 6
B addUniqueColumnFormatter() 0 21 7
B guessMissingColumnFormatters() 0 26 9
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

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 1
    public function __construct(
78
        FakerDataFillerFactory $fakerDataFillerFactory,
79
        DoctrineStaticMeta $testedEntityDsm,
80
        NamespaceHelper $namespaceHelper,
81
        array $fakerDataProviderClasses,
82
        ?float $seed = null
83
    ) {
84 1
        $this->initFakerGenerator($seed);
85 1
        $this->testedEntityDsm          = $testedEntityDsm;
86 1
        $this->fakerDataProviderClasses = $fakerDataProviderClasses;
87 1
        $this->nameGuesser              = new \Faker\Guesser\Name(self::$generator);
88 1
        $this->columnTypeGuesser        = new ColumnTypeGuesser(self::$generator);
89 1
        $this->namespaceHelper          = $namespaceHelper;
90 1
        $this->checkFakerClassesRootNamespaceMatchesEntityFqn(
91 1
            $this->testedEntityDsm->getReflectionClass()->getName()
92
        );
93 1
        $this->generateColumnFormatters();
94 1
        $this->fakerDataFillerFactory = $fakerDataFillerFactory;
95 1
    }
96
97
98
    /**
99
     * @param float|null $seed
100
     * @SuppressWarnings(PHPMD.StaticAccess)
101
     */
102 1
    private function initFakerGenerator(?float $seed): void
103
    {
104 1
        if (null === self::$generator) {
105 1
            self::$generator = Faker\Factory::create();
106
        }
107 1
        self::$generator->seed($seed ?? self::DEFAULT_SEED);
108 1
    }
109
110 1
    private function checkFakerClassesRootNamespaceMatchesEntityFqn(string $fakedEntityFqn): void
111
    {
112 1
        if ([] === $this->fakerDataProviderClasses) {
113
            return;
114
        }
115 1
        $projectRootNamespace = null;
116 1
        foreach (array_keys($this->fakerDataProviderClasses) as $classField) {
117 1
            if (false === \ts\stringContains($classField, '-')) {
118
                continue;
119
            }
120 1
            list($entityFqn,) = explode('-', $classField);
121 1
            $rootNamespace = $this->namespaceHelper->getProjectNamespaceRootFromEntityFqn($entityFqn);
122 1
            if (null === $projectRootNamespace) {
123 1
                $projectRootNamespace = $rootNamespace;
124 1
                continue;
125
            }
126 1
            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 1
        if (null === $projectRootNamespace) {
138
            return;
139
        }
140 1
        $fakedEntityRootNamespace = $this->namespaceHelper->getProjectNamespaceRootFromEntityFqn($fakedEntityFqn);
141 1
        if ($fakedEntityRootNamespace === $projectRootNamespace) {
142 1
            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 1
    private function generateColumnFormatters(): void
154
    {
155 1
        $entityFqn  = $this->testedEntityDsm->getReflectionClass()->getName();
156 1
        $meta       = $this->testedEntityDsm->getMetaData();
157 1
        $fieldNames = $meta->getFieldNames();
158 1
        foreach ($fieldNames as $fieldName) {
159 1
            if (isset($this->columnFormatters[$fieldName])) {
160
                continue;
161
            }
162 1
            if (true === $this->addFakerDataProviderToColumnFormatters($fieldName, $entityFqn)) {
163 1
                continue;
164
            }
165 1
            $fieldMapping = $meta->getFieldMapping($fieldName);
166 1
            if (true === ($fieldMapping['unique'] ?? false)) {
167 1
                $this->addUniqueColumnFormatter($fieldMapping, $fieldName);
168 1
                continue;
169
            }
170
        }
171 1
        $this->guessMissingColumnFormatters();
172 1
    }
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 1
    private function addFakerDataProviderToColumnFormatters(
186
        string $fieldName,
187
        string $entityFqn
188
    ): bool {
189
        foreach ([
190 1
                     $entityFqn . '-' . $fieldName,
191 1
                     $fieldName,
192
                 ] as $key) {
193 1
            if (!isset($this->fakerDataProviderClasses[$key])) {
194 1
                continue;
195
            }
196 1
            if (!isset($this->fakerDataProviderObjects[$key])) {
197 1
                $class                                = $this->fakerDataProviderClasses[$key];
198 1
                $this->fakerDataProviderObjects[$key] = new $class(self::$generator);
199
            }
200 1
            $this->columnFormatters[$fieldName] = $this->fakerDataProviderObjects[$key];
201
202 1
            return true;
203
        }
204
205 1
        return false;
206
    }
207
208 1
    private function addUniqueColumnFormatter(array &$fieldMapping, string $fieldName): void
209
    {
210 1
        switch ($fieldMapping['type']) {
211
            case MappingHelper::TYPE_UUID:
212
            case MappingHelper::TYPE_NON_ORDERED_BINARY_UUID:
213
            case MappingHelper::TYPE_NON_BINARY_UUID:
214 1
                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 1
    private function guessMissingColumnFormatters(): void
249
    {
250
251 1
        $meta = $this->testedEntityDsm->getMetaData();
252 1
        foreach ($meta->getFieldNames() as $fieldName) {
253 1
            if (isset($this->columnFormatters[$fieldName])) {
254 1
                continue;
255
            }
256 1
            if ($meta->isIdentifier($fieldName) || !$meta->hasField($fieldName)) {
257 1
                continue;
258
            }
259 1
            if (false !== \ts\stringContains($fieldName, '.')) {
260
                continue;
261
            }
262
263 1
            $size = $meta->fieldMappings[$fieldName]['length'] ?? null;
264 1
            if (null !== $formatter = $this->guessByName($fieldName, $size)) {
265
                $this->columnFormatters[$fieldName] = $formatter;
266
                continue;
267
            }
268 1
            if (null !== $formatter = $this->columnTypeGuesser->guessFormat($fieldName, $meta)) {
269 1
                $this->columnFormatters[$fieldName] = $formatter;
270 1
                continue;
271
            }
272 1
            if ('json' === $meta->fieldMappings[$fieldName]['type']) {
273 1
                $this->columnFormatters[$fieldName] = $this->getJson();
274
            }
275
        }
276 1
    }
277
278 1
    private function guessByName(string $fieldName, ?int $size): ?\Closure
279
    {
280 1
        $formatter = $this->nameGuesser->guessFormat($fieldName, $size);
281 1
        if (null !== $formatter) {
0 ignored issues
show
introduced by
The condition null !== $formatter is always true.
Loading history...
282
            return $formatter;
283
        }
284 1
        if (false !== \ts\stringContains($fieldName, 'email')) {
285
            return function () {
286
                return self::$generator->email;
287
            };
288
        }
289
290 1
        return null;
291
    }
292
293 1
    private function getJson(): string
294
    {
295 1
        $toEncode                     = [];
296 1
        $toEncode['string']           = self::$generator->text;
297 1
        $toEncode['float']            = self::$generator->randomFloat();
298 1
        $toEncode['nested']['string'] = self::$generator->text;
299 1
        $toEncode['nested']['float']  = self::$generator->randomFloat();
300
301 1
        return json_encode($toEncode, JSON_PRETTY_PRINT);
302
    }
303
304 1
    public function updateDtoWithFakeData(DataTransferObjectInterface $dto): void
305
    {
306 1
        $this->update($dto, true);
307 1
    }
308
309
    /**
310
     * @param DataTransferObjectInterface $dto
311
     * @param bool                        $isRootDto
312
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
313
     */
314 1
    public function update(DataTransferObjectInterface $dto, $isRootDto = false): void
315
    {
316 1
        if (true === $isRootDto) {
317 1
            self::$processedDtos = [];
318
        }
319 1
        if (true === $this->processed($dto)) {
320
            return;
321
        }
322
323 1
        $dtoHash                       = spl_object_hash($dto);
324 1
        self::$processedDtos[$dtoHash] = true;
325 1
        $this->updateFieldsWithFakeData($dto);
326 1
        $this->updateNestedDtosWithFakeData($dto);
327 1
    }
328
329 1
    private function processed(DataTransferObjectInterface $dto): bool
330
    {
331 1
        return array_key_exists(spl_object_hash($dto), self::$processedDtos);
332
    }
333
334 1
    private function updateFieldsWithFakeData(DataTransferObjectInterface $dto): void
335
    {
336 1
        if (null === $this->columnFormatters) {
337
            return;
338
        }
339 1
        foreach ($this->columnFormatters as $field => $formatter) {
340 1
            if (null === $formatter) {
341
                continue;
342
            }
343
            try {
344 1
                $value  = \is_callable($formatter) ? $formatter($dto) : $formatter;
345 1
                $setter = 'set' . $field;
346 1
                $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 1
    }
359
360
    /**
361
     * @param DataTransferObjectInterface $dto
362
     *
363
     * @throws \ReflectionException
364
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
365
     */
366 1
    private function updateNestedDtosWithFakeData(DataTransferObjectInterface $dto): void
367
    {
368 1
        $reflection = new ReflectionClass(\get_class($dto));
369
370 1
        $reflectionMethods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
371 1
        foreach ($reflectionMethods as $reflectionMethod) {
372 1
            $reflectionMethodReturnType = $reflectionMethod->getReturnType();
373 1
            if (null === $reflectionMethodReturnType) {
374 1
                continue;
375
            }
376 1
            $returnTypeName = $reflectionMethodReturnType->getName();
377 1
            $methodName     = $reflectionMethod->getName();
378 1
            if (false === \ts\stringStartsWith($methodName, 'get')) {
379 1
                continue;
380
            }
381 1
            if (substr($returnTypeName, -3) === 'Dto') {
382 1
                $isDtoMethod = 'isset' . substr($methodName, 3, -3) . 'AsDto';
383 1
                if (false === $dto->$isDtoMethod()) {
384 1
                    continue;
385
                }
386
                $got = $dto->$methodName();
387
                if ($got instanceof DataTransferObjectInterface) {
388
                    $this->updateNestedDtoUsingNewFakerFiller($got);
389
                }
390
                continue;
391
            }
392 1
            if ($returnTypeName === Collection::class) {
393
                /**
394
                 * @var Collection
395
                 */
396 1
                $collection = $dto->$methodName();
397 1
                foreach ($collection as $got) {
398 1
                    if ($got instanceof DataTransferObjectInterface) {
399 1
                        $this->updateNestedDtoUsingNewFakerFiller($got);
400
                    }
401
                }
402 1
                continue;
403
            }
404
        }
405 1
    }
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 1
    private function updateNestedDtoUsingNewFakerFiller(DataTransferObjectInterface $dto): void
413
    {
414 1
        $dtoFqn = \get_class($dto);
415 1
        $this->fakerDataFillerFactory
416 1
            ->getInstanceFromDataTransferObjectFqn($dtoFqn)
417 1
            ->update($dto, false);
418 1
    }
419
}
420