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 (#225)
by joseph
18:50
created

FakerDataFiller::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 1.0588

Importance

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

145
                    /** @scrutinizer ignore-type */ $projectRootNamespace .
Loading history...
146
                    ', do we have mixed fakerProviderClasses? ' .
147
                    print_r($this->fakerDataProviderClasses, true)
148
                );
149
            }
150
        }
151 1
        if (null === $projectRootNamespace) {
152
            return;
153
        }
154 1
        $fakedEntityRootNamespace = $this->namespaceHelper->getProjectNamespaceRootFromEntityFqn($fakedEntityFqn);
155 1
        if ($fakedEntityRootNamespace === $projectRootNamespace) {
156 1
            return;
157
        }
158
        throw new RuntimeException('Faked entity FQN ' .
159
                                   $fakedEntityFqn .
160
                                   ' project root namespace does not match the faker classes root namespace ' .
161
                                   $projectRootNamespace);
162
    }
163
164
    /**
165
     * @throws MappingException
166
     */
167 1
    private function generateColumnFormatters(): void
168
    {
169 1
        $entityFqn  = $this->testedEntityDsm->getReflectionClass()->getName();
170 1
        $meta       = $this->testedEntityDsm->getMetaData();
171 1
        $fieldNames = $meta->getFieldNames();
172 1
        foreach ($fieldNames as $fieldName) {
173 1
            if (isset($this->columnFormatters[$fieldName])) {
174
                continue;
175
            }
176 1
            if (true === $this->addFakerDataProviderToColumnFormatters($fieldName, $entityFqn)) {
177
                continue;
178
            }
179 1
            $fieldMapping = $meta->getFieldMapping($fieldName);
180 1
            if (true === ($fieldMapping['unique'] ?? false)) {
181 1
                $this->addUniqueColumnFormatter($fieldMapping, $fieldName);
182 1
                continue;
183
            }
184
        }
185 1
        $this->guessMissingColumnFormatters();
186 1
    }
187
188
    /**
189
     * Add a faker data provider to the columnFormatters array (by reference) if there is one available
190
     *
191
     * Handles instantiating and caching of the data providers
192
     *
193
     * @param string $fieldName
194
     *
195
     * @param string $entityFqn
196
     *
197
     * @return bool
198
     */
199 1
    private function addFakerDataProviderToColumnFormatters(
200
        string $fieldName,
201
        string $entityFqn
202
    ): bool {
203
        foreach (
204
            [
205 1
                     $entityFqn . '-' . $fieldName,
206 1
                     $fieldName,
207
                 ] as $key
208
        ) {
209 1
            if (!isset($this->fakerDataProviderClasses[$key])) {
210 1
                continue;
211
            }
212
            if (!isset($this->fakerDataProviderObjects[$key])) {
213
                $class                                = $this->fakerDataProviderClasses[$key];
214
                $this->fakerDataProviderObjects[$key] = new $class(self::$generator);
215
            }
216
            $this->columnFormatters[$fieldName] = $this->fakerDataProviderObjects[$key];
217
218
            return true;
219
        }
220
221 1
        return false;
222
    }
223
224 1
    private function addUniqueColumnFormatter(array &$fieldMapping, string $fieldName): void
225
    {
226 1
        switch ($fieldMapping['type']) {
227
            case MappingHelper::TYPE_UUID:
228
            case MappingHelper::TYPE_NON_ORDERED_BINARY_UUID:
229
            case MappingHelper::TYPE_NON_BINARY_UUID:
230 1
                return;
231
            case MappingHelper::TYPE_STRING:
232
                $this->columnFormatters[$fieldName] = function () {
233
                    return $this->getUniqueString();
234
                };
235
                break;
236
            case MappingHelper::TYPE_INTEGER:
237
            case Type::BIGINT:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::BIGINT has been deprecated: Use {@see DefaultTypes::BIGINT} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

237
            case /** @scrutinizer ignore-deprecated */ Type::BIGINT:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
238
                $this->columnFormatters[$fieldName] = function () {
239
                    return $this->getUniqueInt();
240
                };
241
                break;
242
            default:
243
                throw new InvalidArgumentException('unique field has an unsupported type: '
244
                                                   . print_r($fieldMapping, true));
245
        }
246
    }
247
248
    private function getUniqueString(): string
249
    {
250
        $string = 'unique string: ' . $this->getUniqueInt() . md5((string)time());
251
        while (isset(self::$uniqueStrings[$string])) {
252
            $string                       = md5((string)time());
253
            self::$uniqueStrings[$string] = true;
254
        }
255
256
        return $string;
257
    }
258
259
    private function getUniqueInt(): int
260
    {
261
        return ++self::$uniqueInt;
262
    }
263
264
    /**
265
     * @SuppressWarnings(PHPMD) - it can't seem to handle this method
266
     */
267 1
    private function guessMissingColumnFormatters(): void
268
    {
269 1
        $meta = $this->testedEntityDsm->getMetaData();
270 1
        foreach ($meta->getFieldNames() as $fieldName) {
271
            if (
272 1
                isset($this->columnFormatters[$fieldName])
273 1
                || $meta->isIdentifier($fieldName)
274 1
                || !$meta->hasField($fieldName)
275 1
                || false !== \ts\stringContains($fieldName, '.')
276 1
                || null === $this->testedEntityDsm->getSetterNameFromPropertyName($fieldName)
277
            ) {
278 1
                continue;
279
            }
280 1
            $size = $meta->fieldMappings[$fieldName]['length'] ?? null;
281 1
            if (null !== $formatter = $this->guessByName($fieldName, $size)) {
282
                $this->columnFormatters[$fieldName] = $formatter;
283
                continue;
284
            }
285 1
            if (null !== $formatter = $this->columnTypeGuesser->guessFormat($fieldName, $meta)) {
286 1
                $this->columnFormatters[$fieldName] = $formatter;
287 1
                continue;
288
            }
289 1
            if (MappingHelper::TYPE_ARRAY === $meta->fieldMappings[$fieldName]['type']) {
290
                $this->columnFormatters[$fieldName] = $this->getArray();
291
            }
292 1
            if (MappingHelper::TYPE_OBJECT === $meta->fieldMappings[$fieldName]['type']) {
293
                $this->columnFormatters[$fieldName] = $this->getObject();
294
            }
295
        }
296 1
    }
297
298 1
    private function guessByName(string $fieldName, ?int $size): ?Closure
299
    {
300 1
        $formatter = $this->nameGuesser->guessFormat($fieldName, $size);
301 1
        if (null !== $formatter) {
0 ignored issues
show
introduced by
The condition null !== $formatter is always true.
Loading history...
302
            return $formatter;
303
        }
304 1
        if (false !== \ts\stringContains($fieldName, 'email')) {
305
            return static function () {
306
                return self::$generator->email;
307
            };
308
        }
309
310 1
        return null;
311
    }
312
313
    /**
314
     * Json should not be a string, it should be data that is then encoded to Json by the Json Type
315
     *
316
     * @return callable
317
     * @see \Doctrine\DBAL\Types\JsonType::convertToDatabaseValue
318
     */
319
    private function getArray(): callable
320
    {
321
        return static function () {
322
            $toEncode                     = [];
323
            $toEncode['string']           = self::$generator->text;
324
            $toEncode['float']            = self::$generator->randomFloat();
325
            $toEncode['nested']['string'] = self::$generator->text;
326
            $toEncode['nested']['float']  = self::$generator->randomFloat();
327
328
            return $toEncode;
329
        };
330
    }
331
332
    private function getObject(): callable
333
    {
334
        return static function () {
335
            $toEncode                 = new stdClass();
336
            $toEncode->string         = self::$generator->text;
337
            $toEncode->float          = self::$generator->randomFloat();
338
            $toEncode->nested->string = self::$generator->text;
339
            $toEncode->nested->float  = self::$generator->randomFloat();
340
341
            return $toEncode;
342
        };
343
    }
344
345 1
    public function updateDtoWithFakeData(DataTransferObjectInterface $dto): void
346
    {
347 1
        $this->update($dto, true);
348 1
    }
349
350
    /**
351
     * @param DataTransferObjectInterface $dto
352
     * @param bool                        $isRootDto
353
     *
354
     * @throws ReflectionException
355
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
356
     */
357 1
    public function update(DataTransferObjectInterface $dto, $isRootDto = false): void
358
    {
359 1
        if (true === $isRootDto) {
360 1
            self::$processedDtos = [];
361
        }
362 1
        if (true === $this->processed($dto)) {
363
            return;
364
        }
365
366 1
        $dtoHash                       = spl_object_hash($dto);
367 1
        self::$processedDtos[$dtoHash] = true;
368 1
        $this->updateFieldsWithFakeData($dto);
369 1
        $this->updateNestedDtosWithFakeData($dto);
370 1
    }
371
372 1
    private function processed(DataTransferObjectInterface $dto): bool
373
    {
374 1
        return array_key_exists(spl_object_hash($dto), self::$processedDtos);
375
    }
376
377 1
    private function updateFieldsWithFakeData(DataTransferObjectInterface $dto): void
378
    {
379 1
        if (null === $this->columnFormatters) {
380
            return;
381
        }
382 1
        foreach ($this->columnFormatters as $field => $formatter) {
383 1
            if (null === $formatter) {
384
                continue;
385
            }
386
            try {
387 1
                $value  = is_callable($formatter) ? $formatter($dto) : $formatter;
388 1
                $setter = 'set' . $field;
389 1
                $dto->$setter($value);
390
            } catch (InvalidArgumentException $ex) {
391
                throw new InvalidArgumentException(
392
                    sprintf(
393
                        'Failed to generate a value for %s::%s: %s',
394
                        get_class($dto),
395
                        $field,
396
                        $ex->getMessage()
397
                    )
398
                );
399
            }
400
        }
401 1
    }
402
403
    /**
404
     * @param DataTransferObjectInterface $dto
405
     *
406
     * @throws ReflectionException
407
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
408
     */
409 1
    private function updateNestedDtosWithFakeData(DataTransferObjectInterface $dto): void
410
    {
411 1
        $reflection = new ReflectionClass(get_class($dto));
412
413 1
        $reflectionMethods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
414 1
        foreach ($reflectionMethods as $reflectionMethod) {
415 1
            $reflectionMethodReturnType = $reflectionMethod->getReturnType();
416 1
            if (null === $reflectionMethodReturnType) {
417 1
                continue;
418
            }
419 1
            $returnTypeName = $reflectionMethodReturnType->getName();
420 1
            $methodName     = $reflectionMethod->getName();
421 1
            if (false === \ts\stringStartsWith($methodName, 'get')) {
422 1
                continue;
423
            }
424 1
            if (substr($returnTypeName, -3) === 'Dto') {
425 1
                $isDtoMethod = 'isset' . substr($methodName, 3, -3) . 'AsDto';
426 1
                if (false === $dto->$isDtoMethod()) {
427 1
                    continue;
428
                }
429
                $got = $dto->$methodName();
430
                if ($got instanceof DataTransferObjectInterface) {
431
                    $this->updateNestedDtoUsingNewFakerFiller($got);
432
                }
433
                continue;
434
            }
435 1
            if ($returnTypeName === Collection::class) {
436
                /**
437
                 * @var Collection
438
                 */
439 1
                $collection = $dto->$methodName();
440 1
                foreach ($collection as $got) {
441
                    if ($got instanceof DataTransferObjectInterface) {
442
                        $this->updateNestedDtoUsingNewFakerFiller($got);
443
                    }
444
                }
445 1
                continue;
446
            }
447
        }
448 1
    }
449
450
    /**
451
     * Get an instance of the Faker filler for this DTO, but do not regard it as root
452
     *
453
     * @param DataTransferObjectInterface $dto
454
     */
455
    private function updateNestedDtoUsingNewFakerFiller(DataTransferObjectInterface $dto): void
456
    {
457
        $dtoFqn = get_class($dto);
458
        $this->fakerDataFillerFactory
459
            ->getInstanceFromDataTransferObjectFqn($dtoFqn)
460
            ->update($dto);
461
    }
462
}
463