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

FakerDataFiller::guessMissingColumnFormatters()   B

Complexity

Conditions 11
Paths 8

Size

Total Lines 26
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 17.8835

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 19
c 1
b 0
f 0
nc 8
nop 0
dl 0
loc 26
rs 7.3166
ccs 16
cts 26
cp 0.6153
crap 17.8835

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

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