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 ( 359331...34f827 )
by joseph
21s queued 18s
created

FakerDataFiller::updateNestedDtosWithFakeData()   B

Complexity

Conditions 10
Paths 10

Size

Total Lines 37
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 17.6332

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
ccs 19
cts 33
cp 0.5758
crap 17.6332
rs 7.6666

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