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
Push — master ( 07a5d8...b7113a )
by joseph
32:24 queued 04:34
created

FakerDataFiller::generateColumnFormatters()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.7873

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 5
nop 0
dl 0
loc 19
ccs 13
cts 19
cp 0.6842
crap 5.7873
rs 9.5222
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
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 1
    public function __construct(
78
        FakerDataFillerFactory $fakerDataFillerFactory,
79
        DoctrineStaticMeta $testedEntityDsm,
80
        NamespaceHelper $namespaceHelper,
81
        array $fakerDataProviderClasses,
82
        ?float $seed = null
83
    ) {
84 1
        $this->initFakerGenerator($seed);
85 1
        $this->testedEntityDsm          = $testedEntityDsm;
86 1
        $this->fakerDataProviderClasses = $fakerDataProviderClasses;
87 1
        $this->nameGuesser              = new \Faker\Guesser\Name(self::$generator);
88 1
        $this->columnTypeGuesser        = new ColumnTypeGuesser(self::$generator);
89 1
        $this->namespaceHelper          = $namespaceHelper;
90 1
        $this->checkFakerClassesRootNamespaceMatchesEntityFqn(
91 1
            $this->testedEntityDsm->getReflectionClass()->getName()
92
        );
93 1
        $this->generateColumnFormatters();
94 1
        $this->fakerDataFillerFactory = $fakerDataFillerFactory;
95 1
    }
96
97
98
    /**
99
     * @param float|null $seed
100
     * @SuppressWarnings(PHPMD.StaticAccess)
101
     */
102 1
    private function initFakerGenerator(?float $seed): void
103
    {
104 1
        if (null === self::$generator) {
105 1
            self::$generator = Faker\Factory::create();
106
        }
107 1
        self::$generator->seed($seed ?? self::DEFAULT_SEED);
108 1
    }
109
110 1
    private function checkFakerClassesRootNamespaceMatchesEntityFqn(string $fakedEntityFqn): void
111
    {
112 1
        if ([] === $this->fakerDataProviderClasses) {
113
            return;
114
        }
115 1
        $projectRootNamespace = null;
116 1
        foreach (array_keys($this->fakerDataProviderClasses) as $classField) {
117 1
            if (false === \ts\stringContains($classField, '-')) {
118
                continue;
119
            }
120 1
            list($entityFqn,) = explode('-', $classField);
121 1
            $rootNamespace = $this->namespaceHelper->getProjectNamespaceRootFromEntityFqn($entityFqn);
122 1
            if (null === $projectRootNamespace) {
123 1
                $projectRootNamespace = $rootNamespace;
124 1
                continue;
125
            }
126 1
            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 1
                    print_r($this->fakerDataProviderClasses, true)
134
                );
135
            }
136
        }
137 1
        if (null === $projectRootNamespace) {
138
            return;
139
        }
140 1
        $fakedEntityRootNamespace = $this->namespaceHelper->getProjectNamespaceRootFromEntityFqn($fakedEntityFqn);
141 1
        if ($fakedEntityRootNamespace === $projectRootNamespace) {
142 1
            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 1
    private function generateColumnFormatters(): void
154
    {
155 1
        $entityFqn  = $this->testedEntityDsm->getReflectionClass()->getName();
156 1
        $meta       = $this->testedEntityDsm->getMetaData();
157 1
        $fieldNames = $meta->getFieldNames();
158 1
        foreach ($fieldNames as $fieldName) {
159 1
            if (isset($this->columnFormatters[$fieldName])) {
160
                continue;
161
            }
162 1
            if (true === $this->addFakerDataProviderToColumnFormatters($fieldName, $entityFqn)) {
163 1
                continue;
164
            }
165 1
            $fieldMapping = $meta->getFieldMapping($fieldName);
166 1
            if (true === ($fieldMapping['unique'] ?? false)) {
167 1
                $this->addUniqueColumnFormatter($fieldMapping, $fieldName);
168 1
                continue;
169
            }
170
        }
171 1
        $this->guessMissingColumnFormatters();
172 1
    }
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 1
    private function addFakerDataProviderToColumnFormatters(
186
        string $fieldName,
187
        string $entityFqn
188
    ): bool {
189
        foreach ([
190 1
                     $entityFqn . '-' . $fieldName,
191 1
                     $fieldName,
192
                 ] as $key) {
193 1
            if (!isset($this->fakerDataProviderClasses[$key])) {
194 1
                continue;
195
            }
196 1
            if (!isset($this->fakerDataProviderObjects[$key])) {
197 1
                $class                                = $this->fakerDataProviderClasses[$key];
198 1
                $this->fakerDataProviderObjects[$key] = new $class(self::$generator);
199
            }
200 1
            $this->columnFormatters[$fieldName] = $this->fakerDataProviderObjects[$key];
201
202 1
            return true;
203
        }
204
205 1
        return false;
206
    }
207
208 1
    private function addUniqueColumnFormatter(array &$fieldMapping, string $fieldName): void
209
    {
210 1
        switch ($fieldMapping['type']) {
211
            case MappingHelper::TYPE_UUID:
212 1
                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 1
    private function guessMissingColumnFormatters(): void
247
    {
248
249 1
        $meta = $this->testedEntityDsm->getMetaData();
250 1
        foreach ($meta->getFieldNames() as $fieldName) {
251 1
            if (isset($this->columnFormatters[$fieldName])) {
252 1
                continue;
253
            }
254 1
            if ($meta->isIdentifier($fieldName) || !$meta->hasField($fieldName)) {
255 1
                continue;
256
            }
257 1
            if (false !== \ts\stringContains($fieldName, '.')) {
258
                continue;
259
            }
260
261 1
            $size = $meta->fieldMappings[$fieldName]['length'] ?? null;
262 1
            if (null !== $formatter = $this->guessByName($fieldName, $size)) {
263
                $this->columnFormatters[$fieldName] = $formatter;
264
                continue;
265
            }
266 1
            if (null !== $formatter = $this->columnTypeGuesser->guessFormat($fieldName, $meta)) {
267 1
                $this->columnFormatters[$fieldName] = $formatter;
268 1
                continue;
269
            }
270 1
            if ('json' === $meta->fieldMappings[$fieldName]['type']) {
271 1
                $this->columnFormatters[$fieldName] = $this->getJson();
272
            }
273
        }
274 1
    }
275
276 1
    private function guessByName(string $fieldName, ?int $size): ?\Closure
277
    {
278 1
        $formatter = $this->nameGuesser->guessFormat($fieldName, $size);
279 1
        if (null !== $formatter) {
0 ignored issues
show
introduced by
The condition null !== $formatter is always true.
Loading history...
280
            return $formatter;
281
        }
282 1
        if (false !== \ts\stringContains($fieldName, 'email')) {
283
            return function () {
284
                return self::$generator->email;
285
            };
286
        }
287
288 1
        return null;
289
    }
290
291 1
    private function getJson(): string
292
    {
293 1
        $toEncode                     = [];
294 1
        $toEncode['string']           = self::$generator->text;
295 1
        $toEncode['float']            = self::$generator->randomFloat();
296 1
        $toEncode['nested']['string'] = self::$generator->text;
297 1
        $toEncode['nested']['float']  = self::$generator->randomFloat();
298
299 1
        return json_encode($toEncode, JSON_PRETTY_PRINT);
300
    }
301
302 1
    public function updateDtoWithFakeData(DataTransferObjectInterface $dto): void
303
    {
304 1
        $this->update($dto, true);
305 1
    }
306
307
    /**
308
     * @param DataTransferObjectInterface $dto
309
     * @param bool                        $isRootDto
310
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
311
     */
312 1
    private function update(DataTransferObjectInterface $dto, $isRootDto = false)
313
    {
314 1
        if (true === $isRootDto) {
315 1
            self::$processedDtos = [];
316
        }
317 1
        if (true === $this->processed($dto)) {
318
            return;
319
        }
320
321 1
        $dtoHash                       = spl_object_hash($dto);
322 1
        self::$processedDtos[$dtoHash] = true;
323 1
        $this->updateFieldsWithFakeData($dto);
324 1
        $this->updateNestedDtosWithFakeData($dto);
325 1
    }
326
327 1
    private function processed(DataTransferObjectInterface $dto): bool
328
    {
329 1
        return array_key_exists(spl_object_hash($dto), self::$processedDtos);
330
    }
331
332 1
    private function updateFieldsWithFakeData(DataTransferObjectInterface $dto): void
333
    {
334 1
        if (null === $this->columnFormatters) {
335
            return;
336
        }
337 1
        foreach ($this->columnFormatters as $field => $formatter) {
338 1
            if (null === $formatter) {
339
                continue;
340
            }
341
            try {
342 1
                $value  = \is_callable($formatter) ? $formatter($dto) : $formatter;
343 1
                $setter = 'set' . $field;
344 1
                $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 1
                        $ex->getMessage()
352
                    )
353
                );
354
            }
355
        }
356 1
    }
357
358
    /**
359
     * @param DataTransferObjectInterface $dto
360
     *
361
     * @throws \ReflectionException
362
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
363
     */
364 1
    private function updateNestedDtosWithFakeData(DataTransferObjectInterface $dto): void
365
    {
366 1
        $reflection = new ReflectionClass(\get_class($dto));
367
368 1
        $reflectionMethods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
369 1
        foreach ($reflectionMethods as $reflectionMethod) {
370 1
            $reflectionMethodReturnType = $reflectionMethod->getReturnType();
371 1
            if (null === $reflectionMethodReturnType) {
372 1
                continue;
373
            }
374 1
            $returnTypeName = $reflectionMethodReturnType->getName();
375 1
            $methodName     = $reflectionMethod->getName();
376 1
            if (false === \ts\stringStartsWith($methodName, 'get')) {
377 1
                continue;
378
            }
379 1
            if (substr($returnTypeName, -3) === 'Dto') {
380 1
                $isDtoMethod = 'isset' . substr($methodName, 3, -3) . 'AsDto';
381 1
                if (false === $dto->$isDtoMethod()) {
382 1
                    continue;
383
                }
384
                $got = $dto->$methodName();
385
                if ($got instanceof DataTransferObjectInterface) {
386
                    $this->updateNestedDtoUsingNewFakerFiller($got);
387
                }
388
                continue;
389
            }
390 1
            if ($returnTypeName === Collection::class) {
391
                /**
392
                 * @var Collection
393
                 */
394 1
                $collection = $dto->$methodName();
395 1
                foreach ($collection as $got) {
396 1
                    if ($got instanceof DataTransferObjectInterface) {
397 1
                        $this->updateNestedDtoUsingNewFakerFiller($got);
398
                    }
399
                }
400 1
                continue;
401
            }
402
        }
403 1
    }
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 1
    private function updateNestedDtoUsingNewFakerFiller(DataTransferObjectInterface $dto): void
411
    {
412 1
        $dtoFqn = \get_class($dto);
413 1
        $this->fakerDataFillerFactory
414 1
            ->getInstanceFromDataTransferObjectFqn($dtoFqn)
415 1
            ->update($dto, false);
416 1
    }
417
}
418