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 (#199)
by joseph
17:26
created

FakerDataFiller::guessByName()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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

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