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 (#164)
by joseph
23:31
created

FakerDataFiller::update()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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

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