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 (#153)
by joseph
29:27
created

FakerDataFiller::updateNestedDtosWithFakeData()   B

Complexity

Conditions 10
Paths 10

Size

Total Lines 37
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
cc 10
eloc 24
nc 10
nop 1
dl 0
loc 37
rs 7.6666
c 0
b 0
f 0
ccs 0
cts 33
cp 0
crap 110

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\MappingHelper;
11
use Faker;
12
use Faker\ORM\Doctrine\ColumnTypeGuesser;
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(){return $this->getUniqueString();};
215
                break;
216
            case MappingHelper::TYPE_INTEGER:
217
            case Type::BIGINT:
218
                $this->columnFormatters[$fieldName] = function(){return $this->getUniqueInt();};
219
                break;
220
            default:
221
                throw new \InvalidArgumentException('unique field has an unsupported type: '
222
                                                    . print_r($fieldMapping, true));
223
        }
224
    }
225
226
    private function getUniqueString(): string
227
    {
228
        $string = 'unique string: ' . $this->getUniqueInt() . md5((string)time());
229
        while (isset(self::$uniqueStrings[$string])) {
230
            $string                       = md5((string)time());
231
            self::$uniqueStrings[$string] = true;
232
        }
233
234
        return $string;
235
    }
236
237
    private function getUniqueInt(): int
238
    {
239
        return ++self::$uniqueInt;
240
    }
241
242
    private function guessMissingColumnFormatters(): void
243
    {
244
245
        $meta = $this->testedEntityDsm->getMetaData();
246
        foreach ($meta->getFieldNames() as $fieldName) {
247
            if (isset($this->columnFormatters[$fieldName])) {
248
                continue;
249
            }
250
            if ($meta->isIdentifier($fieldName) || !$meta->hasField($fieldName)) {
251
                continue;
252
            }
253
            if (false !== \ts\stringContains($fieldName, '.')) {
254
                continue;
255
            }
256
257
            $size = $meta->fieldMappings[$fieldName]['length'] ?? null;
258
            if (null !== $formatter = $this->guessByName($fieldName, $size)) {
259
                $this->columnFormatters[$fieldName] = $formatter;
260
                continue;
261
            }
262
            if (null !== $formatter = $this->columnTypeGuesser->guessFormat($fieldName, $meta)) {
263
                $this->columnFormatters[$fieldName] = $formatter;
264
                continue;
265
            }
266
            if ('json' === $meta->fieldMappings[$fieldName]['type']) {
267
                $this->columnFormatters[$fieldName] = $this->getJson();
268
            }
269
        }
270
    }
271
272
    private function guessByName(string $fieldName, ?int $size): ?\Closure
273
    {
274
        $formatter = $this->nameGuesser->guessFormat($fieldName, $size);
275
        if (null !== $formatter) {
0 ignored issues
show
introduced by
The condition null !== $formatter is always true.
Loading history...
276
            return $formatter;
277
        }
278
        if (false !== \ts\stringContains($fieldName, 'email')) {
279
            return function () {
280
                return self::$generator->email;
281
            };
282
        }
283
284
        return null;
285
    }
286
287
    private function getJson(): string
288
    {
289
        $toEncode                     = [];
290
        $toEncode['string']           = self::$generator->text;
291
        $toEncode['float']            = self::$generator->randomFloat();
292
        $toEncode['nested']['string'] = self::$generator->text;
293
        $toEncode['nested']['float']  = self::$generator->randomFloat();
294
295
        return json_encode($toEncode, JSON_PRETTY_PRINT);
296
    }
297
298
    public function updateDtoWithFakeData(DataTransferObjectInterface $dto): void
299
    {
300
        $this->update($dto, true);
301
    }
302
303
    private function update(DataTransferObjectInterface $dto, $isRootDto = false)
304
    {
305
        if (true === $isRootDto) {
306
            self::$processedDtos = [];
307
        }
308
        if (true === $this->processed($dto)) {
309
            return;
310
        }
311
312
        $dtoHash                       = spl_object_hash($dto);
313
        self::$processedDtos[$dtoHash] = true;
314
        $this->updateFieldsWithFakeData($dto);
315
        $this->updateNestedDtosWithFakeData($dto);
316
    }
317
318
    private function processed(DataTransferObjectInterface $dto): bool
319
    {
320
        return array_key_exists(spl_object_hash($dto), self::$processedDtos);
321
    }
322
323
    private function updateFieldsWithFakeData(DataTransferObjectInterface $dto): void
324
    {
325
        if (null === $this->columnFormatters) {
326
            return;
327
        }
328
        foreach ($this->columnFormatters as $field => $formatter) {
329
            if (null === $formatter) {
330
                continue;
331
            }
332
            try {
333
                $value  = \is_callable($formatter) ? $formatter($dto) : $formatter;
334
                $setter = 'set' . $field;
335
                $dto->$setter($value);
336
            } catch (\InvalidArgumentException $ex) {
337
                throw new \InvalidArgumentException(
338
                    sprintf(
339
                        'Failed to generate a value for %s::%s: %s',
340
                        \get_class($dto),
341
                        $field,
342
                        $ex->getMessage()
343
                    )
344
                );
345
            }
346
        }
347
    }
348
349
    private function updateNestedDtosWithFakeData(DataTransferObjectInterface $dto): void
350
    {
351
        $reflection = new ReflectionClass(\get_class($dto));
352
353
        $reflectionMethods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
354
        foreach ($reflectionMethods as $reflectionMethod) {
355
            $reflectionMethodReturnType = $reflectionMethod->getReturnType();
356
            if (null === $reflectionMethodReturnType) {
357
                continue;
358
            }
359
            $returnTypeName = $reflectionMethodReturnType->getName();
360
            $methodName     = $reflectionMethod->getName();
361
            if (false === \ts\stringStartsWith($methodName, 'get')) {
362
                continue;
363
            }
364
            if (substr($returnTypeName, -3) === 'Dto') {
365
                $isDtoMethod = 'isset' . substr($methodName, 3, -3) . 'AsDto';
366
                if (false === $dto->$isDtoMethod()) {
367
                    continue;
368
                }
369
                $got = $dto->$methodName();
370
                if ($got instanceof DataTransferObjectInterface) {
371
                    $this->updateNestedDtoUsingNewFakerFiller($got);
372
                }
373
                continue;
374
            }
375
            if ($returnTypeName === Collection::class) {
376
                /**
377
                 * @var Collection
378
                 */
379
                $collection = $dto->$methodName();
380
                foreach ($collection as $got) {
381
                    if ($got instanceof DataTransferObjectInterface) {
382
                        $this->updateNestedDtoUsingNewFakerFiller($got);
383
                    }
384
                }
385
                continue;
386
            }
387
388
389
        }
390
    }
391
392
    /**
393
     * Get an instance of the Faker filler for this DTO, but do not regard it as root
394
     *
395
     * @param DataTransferObjectInterface $dto
396
     */
397
    private function updateNestedDtoUsingNewFakerFiller(DataTransferObjectInterface $dto): void
398
    {
399
        $dtoFqn = \get_class($dto);
400
        $this->fakerDataFillerFactory
401
            ->getInstanceFromDataTransferObjectFqn($dtoFqn)
402
            ->update($dto, false);
403
    }
404
}
405