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
16:19
created

FakerDataFiller::getObject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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