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.
Passed
Pull Request — master (#214)
by joseph
20:33
created

FakerDataFiller::getJson()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1.0019

Importance

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