Passed
Push — feature-FRAM-58-refactor-entit... ( e3cd10...fc48d1 )
by Vincent
06:00
created

EntityGenerator   F

Complexity

Total Complexity 173

Size/Duplication

Total Lines 1117
Duplicated Lines 0 %

Test Coverage

Coverage 96.01%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 173
eloc 369
c 3
b 1
f 0
dl 0
loc 1117
ccs 361
cts 376
cp 0.9601
rs 2

50 Methods

Rating   Name   Duplication   Size   Complexity  
A generateEntityClass() 0 17 2
A generate() 0 13 6
A __construct() 0 4 1
A generateUpdatedEntityClass() 0 15 1
B hasMethod() 0 21 9
A hasNamespace() 0 3 1
A setTraits() 0 3 1
A setGenerateStubMethods() 0 3 1
C generateEntityFieldMappingProperties() 0 41 14
A getTraits() 0 3 1
A addInterface() 0 3 1
D parseTokensInEntityFile() 0 44 21
A getTraitsReflections() 0 17 3
A getClassName() 0 4 1
A generateAdder() 0 23 2
A getUseConstructorPropertyPromotion() 0 3 1
A addTrait() 0 3 1
A generateSetter() 0 23 2
A getUseTypedProperties() 0 3 1
A generateGetter() 0 15 3
A getInterfaces() 0 3 1
A useConstructorPropertyPromotion() 0 3 1
A getClassToExtend() 0 3 1
A setRegenerateEntityIfExists() 0 3 1
A generateEntityClassDeclaration() 0 13 3
B generateClassicConstructor() 0 27 7
A setFieldVisibility() 0 7 3
A generateConstructorWithPromotedProperties() 0 15 4
A setInterfaces() 0 3 1
A setNumSpaces() 0 3 1
A useGetShortcutMethod() 0 3 1
A hasProperty() 0 20 6
A useTypedProperties() 0 3 1
A generateEntityBody() 0 18 4
A setUpdateEntityIfExists() 0 3 1
A getUseGetShortcutMethod() 0 3 1
A getUpdateEntityIfExists() 0 3 1
B accessorMetadata() 0 59 11
A generateEntityStubMethods() 0 18 6
A getNumSpaces() 0 3 1
C generateEntityEmbeddedProperties() 0 76 17
A getRegenerateEntityIfExists() 0 3 1
A setExtension() 0 3 1
A getExtension() 0 3 1
B generateEntityConstructor() 0 23 7
A getGenerateStubMethods() 0 3 1
C generateEntityUse() 0 34 13
A getFieldVisibility() 0 3 1
A getNamespace() 0 6 1
A setClassToExtend() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like EntityGenerator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EntityGenerator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Bdf\Prime\Entity;
4
5
use Bdf\Prime\Mapper\Info\InfoInterface;
6
use Bdf\Prime\Mapper\Info\MapperInfo;
7
use Bdf\Prime\Mapper\Info\ObjectPropertyInfo;
8
use Bdf\Prime\Mapper\Info\PropertyInfo;
9
use Bdf\Prime\Mapper\Mapper;
10
use Bdf\Prime\ServiceLocator;
11
use Bdf\Prime\Types\PhpTypeInterface;
12
use Doctrine\Inflector\Inflector;
13
use Doctrine\Inflector\Inflector as InflectorObject;
14
use Doctrine\Inflector\InflectorFactory;
15
use Nette\PhpGenerator\ClassType;
16
use Nette\PhpGenerator\Method;
17
use Nette\PhpGenerator\PhpFile;
18
use Nette\PhpGenerator\PhpNamespace;
19
use Nette\PhpGenerator\Printer;
20
use Nette\PhpGenerator\PsrPrinter;
21
22
/**
23
 * Generic class used to generate PHP 7 and 8 entity classes from Mapper.
24
 *
25
 *     [php]
26
 *     $mapper = $service->mappers()->build('Entity');
27
 *
28
 *     $generator = new EntityGenerator();
29
 *     $generator->setGenerateStubMethods(true);
30
 *     $generator->setRegenerateEntityIfExists(false);
31
 *     $generator->setUpdateEntityIfExists(true);
32
 *     $generator->generate($mapper, '/path/to/generate/entities');
33
 *
34
 * @todo version 3.6 et 4.0 pour nette generator
35
 */
36
class EntityGenerator
37
{
38
    // @todo should not be there : should be on PhpTypeInterface
39
    /**
40
     * Map prime types to php 7.4 property type
41
     */
42
    public const PROPERTY_TYPE_MAP = [
43
        PhpTypeInterface::BOOLEAN => 'bool',
44
        PhpTypeInterface::DOUBLE => 'float',
45
        PhpTypeInterface::INTEGER => 'int',
46
    ];
47
48
    /**
49
     * Specifies class fields should be protected.
50
     */
51
    public const FIELD_VISIBLE_PROTECTED = ClassType::VISIBILITY_PROTECTED;
52
53
    /**
54
     * Specifies class fields should be private.
55
     */
56
    public const FIELD_VISIBLE_PRIVATE = ClassType::VISIBILITY_PRIVATE;
57
58
    /**
59
     * The prime service locator
60
     *
61
     * @var ServiceLocator
62
     */
63
    private ServiceLocator $prime;
64
65
    /**
66
     * The inflector instance
67
     *
68
     * @var InflectorObject
69
     */
70
    private InflectorObject $inflector;
71
72
    /**
73
     * The mapper info
74
     *
75
     * @var MapperInfo
76
     */
77
    private MapperInfo $mapperInfo;
78
79
    /**
80
     * The extension to use for written php files.
81
     *
82
     * @var string
83
     */
84
    private string $extension = '.php';
85
86
    /**
87
     * Whether or not the current Mapper instance is new or old.
88
     *
89
     * @var boolean
90
     */
91
    private bool $isNew = true;
92
93
    /**
94
     * @var array<class-string, array{properties: list<string>, methods: list<string>}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, arra...methods: list<string>}> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, array{properties: list<string>, methods: list<string>}>.
Loading history...
95
     */
96
    private array $staticReflection = [];
97
98
    /**
99
     * Number of spaces to use for indention in generated code.
100
     */
101
    private int $numSpaces = 4;
102
103
    /**
104
     * The class all generated entities should extend.
105
     *
106
     * @var class-string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|null at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|null.
Loading history...
107
     */
108
    private ?string $classToExtend = null;
109
110
    /**
111
     * The interfaces all generated entities should implement.
112
     *
113
     * @var array<class-string, class-string>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, class-string> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, class-string>.
Loading history...
114
     */
115
    private array $interfaces = [];
116
117
    /**
118
     * The traits
119
     *
120
     * @var array<string, string>
121
     */
122
    private array $traits = [];
123
124
    /**
125
     * Whether or not to generate sub methods.
126
     *
127
     * @var boolean
128
     */
129
    private bool $generateEntityStubMethods = true;
130
131
    /**
132
     * Whether or not to update the entity class if it exists already.
133
     *
134
     * @var boolean
135
     */
136
    private bool $updateEntityIfExists = false;
137
138
    /**
139
     * Whether or not to re-generate entity class if it exists already.
140
     *
141
     * @var boolean
142
     */
143
    private bool $regenerateEntityIfExists = false;
144
145
    /**
146
     * The name of get methods will not contains the 'get' prefix
147
     *
148
     * @var boolean
149
     */
150
    private bool $useGetShortcutMethod = true;
151
152
    /**
153
     * Visibility of the field
154
     *
155
     * @var self::FIELD_*
156
     */
157
    private string $fieldVisibility = self::FIELD_VISIBLE_PROTECTED;
158
159
    /**
160
     * Use type on generated properties
161
     * Note: only compatible with PHP >= 7.4
162
     *
163
     * @var bool
164
     */
165
    private bool $useTypedProperties = false;
166
167
    /**
168
     * Enable generation of PHP 8 constructor with promoted properties
169
     * If used, the constructor will not call import for filling the entity
170
     *
171
     * Note: only compatible with PHP >= 8.0
172
     *
173
     * @var bool
174
     */
175
    private bool $useConstructorPropertyPromotion = false;
176
177
    /**
178
     * Set prime service locator
179
     */
180 36
    public function __construct(ServiceLocator $prime, ?InflectorObject $inflector = null)
181
    {
182 36
        $this->prime = $prime;
183 36
        $this->inflector = $inflector ?? InflectorFactory::create()->build();
184
    }
185
186
    /**
187
     * Generates and writes entity classes
188
     *
189
     * @param Mapper $mapper
190
     * @param string|null $file Entity file name
191
     *
192
     * @return string|false If no generation
193
     *
194
     * @api
195
     */
196 34
    public function generate(Mapper $mapper, ?string $file = null)
197
    {
198 34
        $this->isNew = !$file || !file_exists($file) || $this->regenerateEntityIfExists;
199
200
        // If entity doesn't exist or we're re-generating the entities entirely
201 34
        if ($this->isNew || !$file) {
202 30
            return $this->generateEntityClass($mapper);
203
        // If entity exists and we're allowed to update the entity class
204 4
        } elseif ($this->updateEntityIfExists) {
205 4
            return $this->generateUpdatedEntityClass($mapper, $file);
206
        }
207
208
        return false;
209
    }
210
211
    /**
212
     * Generates a PHP5 Doctrine 2 entity class from the given Mapper instance.
213
     *
214
     * @param Mapper $mapper
215
     *
216
     * @return string
217
     */
218 31
    public function generateEntityClass(Mapper $mapper): string
219
    {
220 31
        $this->mapperInfo = $mapper->info();
221
222 31
        $file = new PhpFile();
223
224 31
        $namespace = $file->addNamespace($this->hasNamespace($this->mapperInfo->className()) ? $this->getNamespace($this->mapperInfo->className()) : '');
225 31
        $generator = $namespace->addClass($this->getClassName($this->mapperInfo->className()));
226
227 31
        $this->staticReflection[$this->mapperInfo->className()] = ['properties' => [], 'methods' => []];
228
229 31
        $this->generateEntityClassDeclaration($generator);
230 31
        $this->generateEntityUse($namespace);
231 31
        $this->generateEntityBody($generator, $namespace);
232
233 31
        return (new ConfigurableEntityPrinter($this))
234 31
            ->printFile($file);
235
    }
236
237
    /**
238
     * Generates the updated code for the given Mapper and entity at path.
239
     *
240
     * @param Mapper $mapper
241
     * @param string $filename
242
     *
243
     * @return string
244
     */
245 4
    public function generateUpdatedEntityClass(Mapper $mapper, string $filename): string
246
    {
247 4
        $this->mapperInfo = $mapper->info();
248
249 4
        $currentCode = file_get_contents($filename);
250 4
        $file = PhpFile::fromCode($currentCode);
251 4
        $generator = $file->getClasses()[$this->mapperInfo->className()];
252 4
        $namespace = $file->getNamespaces()[$this->getNamespace($this->mapperInfo->className())];
253
254 4
        $this->parseTokensInEntityFile($currentCode);
255
256 4
        $this->generateEntityBody($generator, $namespace);
257
258 4
        return (new ConfigurableEntityPrinter($this))
259 4
            ->printFile($file);
260
    }
261
262
    /**
263
     * Generate class inheritance and traits
264
     */
265 31
    protected function generateEntityClassDeclaration(ClassType $generator): void
266
    {
267 31
        $generator->addComment($this->getClassName($this->mapperInfo->className()));
268
269 31
        if ($this->classToExtend) {
270 2
            $generator->setExtends($this->classToExtend);
271
        }
272
273 31
        foreach ($this->interfaces as $interface) {
274 3
            $generator->addImplement($interface);
275
        }
276
277 31
        $generator->setTraits($this->traits);
278
    }
279
280
    /**
281
     * Generate use part
282
     */
283 31
    protected function generateEntityUse(PhpNamespace $namespace): void
284
    {
285 31
        if (($parentClass = $this->getClassToExtend()) && $this->hasNamespace($parentClass)) {
286 2
            $namespace->addUse($parentClass);
287
        }
288
289 31
        foreach ($this->interfaces as $interface) {
290 3
            if ($this->hasNamespace($interface)) {
291 3
                $namespace->addUse($interface);
292
            }
293
        }
294
295 31
        foreach ($this->traits as $trait) {
296 2
            if ($this->hasNamespace($trait)) {
297
                $namespace->addUse($trait);
298
            }
299
        }
300
301 31
        foreach ($this->mapperInfo->objects() as $info) {
302 26
            $className = $info->className();
303 26
            if (!$info->belongsToRoot()) {
304 4
                continue;
305
            }
306
307 26
            if ($this->hasNamespace($className) && $this->getNamespace($className) !== $namespace->getName()) {
0 ignored issues
show
Bug introduced by
It seems like $className can also be of type null; however, parameter $className of Bdf\Prime\Entity\EntityGenerator::getNamespace() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

307
            if ($this->hasNamespace($className) && $this->getNamespace(/** @scrutinizer ignore-type */ $className) !== $namespace->getName()) {
Loading history...
Bug introduced by
It seems like $className can also be of type null; however, parameter $className of Bdf\Prime\Entity\EntityGenerator::hasNamespace() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

307
            if ($this->hasNamespace(/** @scrutinizer ignore-type */ $className) && $this->getNamespace($className) !== $namespace->getName()) {
Loading history...
308
                $namespace->addUse($className);
0 ignored issues
show
Bug introduced by
It seems like $className can also be of type null; however, parameter $name of Nette\PhpGenerator\PhpNamespace::addUse() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

308
                $namespace->addUse(/** @scrutinizer ignore-type */ $className);
Loading history...
309
            }
310
311 26
            if ($info->wrapper() !== null) {
312 1
                $repository = $this->prime->repository($className);
313 1
                $wrapperClass = $repository->collectionFactory()->wrapperClass($info->wrapper());
0 ignored issues
show
Bug introduced by
It seems like $info->wrapper() can also be of type callable; however, parameter $wrapper of Bdf\Prime\Collection\Col...Factory::wrapperClass() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

313
                $wrapperClass = $repository->collectionFactory()->wrapperClass(/** @scrutinizer ignore-type */ $info->wrapper());
Loading history...
314
315 1
                if ($this->hasNamespace($wrapperClass)) {
316 1
                    $namespace->addUse($wrapperClass);
317
                }
318
            }
319
        }
320
    }
321
322 35
    protected function generateEntityBody(ClassType $generator, PhpNamespace $namespace): void
323
    {
324
        $properties = [
325 35
            ...$this->generateEntityFieldMappingProperties($this->useConstructorPropertyPromotion),
326 35
            ...$this->generateEntityEmbeddedProperties($namespace, $this->useConstructorPropertyPromotion)
327
        ];
328
329 35
        if (!$this->useConstructorPropertyPromotion) {
330 33
            foreach ($properties as $property) {
331 31
                $property->addProperty($generator, $namespace);
332
            }
333
        }
334
335 35
        if ($this->generateEntityStubMethods) {
336 34
            $this->generateEntityStubMethods($generator, $namespace);
337
        }
338
339 35
        $this->generateEntityConstructor($generator, $namespace, $this->useConstructorPropertyPromotion, $properties);
340
    }
341
342
    /**
343
     * @param bool $propertyPromotion Generate constructor with property promotion
344
     * @param list<PropertyGenerator> $properties
0 ignored issues
show
Bug introduced by
The type Bdf\Prime\Entity\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
345
     */
346 35
    protected function generateEntityConstructor(ClassType $generator, PhpNamespace $namespace, bool $propertyPromotion, array $properties): void
347
    {
348 35
        $initializable = in_array(InitializableInterface::class, $this->interfaces);
349 35
        $isImportable  = in_array(ImportableInterface::class, $this->interfaces)
350 35
                    || is_subclass_of($this->classToExtend, ImportableInterface::class);
351
352 35
        if (!$this->hasMethod('__construct')) {
353 29
            if ($propertyPromotion) {
354 2
                $this->generateConstructorWithPromotedProperties($generator, $namespace, $initializable, $properties);
355
            } else {
356 27
                $this->generateClassicConstructor($generator, $namespace, $isImportable, $initializable, $properties);
357
            }
358
        }
359
360 35
        if (!$this->hasMethod('initialize') && $initializable) {
361 1
            $init = Method::from([InitializableInterface::class, 'initialize'])
362 1
                ->addComment('{@inheritdoc}');
363
364 1
            foreach ($properties as $property) {
365 1
                $property->addInitializeLine($init);
366
            }
367
368 1
            $generator->addMember($init);
369
        }
370
    }
371
372
    /**
373
     * Generate PHP 8 constructor
374
     *
375
     * @param bool $initializable Does the entity class implements InitializableInterface ?
376
     * @param list<PropertyGenerator> $properties Properties to declare an initialize
377
     */
378 2
    private function generateConstructorWithPromotedProperties(ClassType $generator, PhpNamespace $namespace, bool $initializable, array $properties): void
379
    {
380 2
        $constructor = $generator->addMethod('__construct');
381
382 2
        foreach ($properties as $property) {
383 2
            $property->addPromotedProperty($constructor, $namespace);
384
        }
385
386 2
        if ($initializable) {
387
            $constructor->addBody('$this->initialize();');
388
        } else {
389 2
            foreach ($properties as $property) {
390
                // Assignment operator : use null coalesce assignment with property promotion
391
                // because assignation is performed before initializing default value
392 2
                $property->addInitializeLine($constructor, '??=');
393
            }
394
        }
395
    }
396
397
    /**
398
     * Generate classic constructor
399
     *
400
     * @param bool $isImportable Does the entity class implements InitializableInterface ?
401
     * @param bool $initializable Does the entity class implements ImportableInterface ?
402
     * @param list<PropertyGenerator> $properties Properties to initialize
403
     */
404 27
    private function generateClassicConstructor(ClassType $generator, PhpNamespace $namespace, bool $isImportable, bool $initializable, array $properties): void
0 ignored issues
show
Unused Code introduced by
The parameter $namespace is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

404
    private function generateClassicConstructor(ClassType $generator, /** @scrutinizer ignore-unused */ PhpNamespace $namespace, bool $isImportable, bool $initializable, array $properties): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
405
    {
406 27
        if ($isImportable) {
407 2
            $constructor = $generator->addMethod('__construct');
408
409
            $constructor
410 2
                ->addParameter('data', [])
411 2
                ->setType('array')
412
            ;
413
414 2
            if ($initializable) {
415 1
                $constructor->addBody('$this->initialize();');
416
            } else {
417 1
                foreach ($properties as $property) {
418 1
                    $property->addInitializeLine($constructor);
419
                }
420
            }
421
422 2
            $constructor->addBody('$this->import($data);');
423 25
        } elseif (!$initializable) {
424 25
            $constructor = null;
425
426 25
            foreach ($properties as $property) {
427 25
                if ($property->hasInitialisation()) {
428
                    // Add a constructor only if it's necessary
429 23
                    $constructor = $constructor ?? $generator->addMethod('__construct');
430 23
                    $property->addInitializeLine($constructor);
431
                }
432
            }
433
        }
434
    }
435
436 34
    protected function generateEntityStubMethods(ClassType $generator, PhpNamespace $namespace): void
437
    {
438 34
        foreach ($this->mapperInfo->properties() as $property) {
439 34
            $this->generateSetter($generator, $namespace, $property);
440 34
            $this->generateGetter($generator, $namespace, $property);
441
        }
442
443 34
        foreach ($this->mapperInfo->objects() as $property) {
444 25
            if (!$property->belongsToRoot()) {
445 4
                continue;
446
            }
447
448 25
            if ($property->isArray() && $property->wrapper() === null) {
449 19
                $this->generateAdder($generator, $namespace, $property);
450
            }
451
452 25
            $this->generateSetter($generator, $namespace, $property);
453 25
            $this->generateGetter($generator, $namespace, $property);
454
        }
455
    }
456
457
    /**
458
     * @param bool $forceNullable Force typehint to be nullable. Useful property promotion
459
     * @return list<PropertyGenerator>
460
     */
461 35
    protected function generateEntityFieldMappingProperties(bool $forceNullable = false): array
462
    {
463 35
        $properties = [];
464
465 35
        foreach ($this->mapperInfo->properties() as $property) {
466 35
            if ($this->hasProperty($property->name())) {
467 6
                continue;
468
            }
469
470 33
            $properties[] = $generator = new PropertyGenerator($property->name());
471
472 33
            $generator->setNullable($forceNullable || $property->isNullable());
473 33
            $generator->setVisibility($this->fieldVisibility);
474 33
            $generator->setVarTag($property->phpType());
475
476 33
            if ($this->useTypedProperties) {
477 15
                $generator->setTypeHint($property->phpType());
478
            }
479
480 33
            if ($property->hasDefault() && !$property->isDateTime()) {
481 5
                $generator->setDefaultValue($property->convert($property->getDefault()));
482 33
            } elseif ($property->isArray()) {
483 17
                $generator->setDefaultValue([]);
484 31
            } elseif (($forceNullable || ($this->useTypedProperties && $property->isNullable()))) {
485
                // A nullable property should be defined as null by default
486
                // A property is considered as nullable if it's explicitly defined on mapper or if the field is auto-generated
487 13
                $generator->setDefaultValue(null);
488
            }
489
490 33
            if ($property->hasDefault() && $property->isDateTime()) {
491 4
                $constructorArgs = '';
492
                // Add the default timezone from the property type.
493 4
                if ($timezone = $property->getTimezone()) {
494 4
                    $constructorArgs = "'now', new \DateTimeZone('$timezone')";
495
                }
496
497 4
                $generator->setInitialize('new '.$property->phpType().'('.$constructorArgs.')');
498
            }
499
        }
500
501 35
        return $properties;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $properties returns the type Bdf\Prime\Entity\PropertyGenerator[]|array which is incompatible with the documented return type Bdf\Prime\Entity\list.
Loading history...
502
    }
503
504
    /**
505
     * @param bool $forceNullable Force typehint to be nullable. Useful property promotion
506
     * @return list<PropertyGenerator>
507
     */
508 35
    protected function generateEntityEmbeddedProperties(PhpNamespace $namespace, bool $forceNullable = false): array
509
    {
510 35
        $properties = [];
511
512 35
        foreach ($this->mapperInfo->objects() as $property) {
513 26
            if (!$property->belongsToRoot() || $this->hasProperty($property->name())) {
514 6
                continue;
515
            }
516
517 26
            $properties[] = $generator = new PropertyGenerator($property->name());
518 26
            $generator->setVisibility($this->fieldVisibility);
519
520
            // Embedded property : should not be null
521 26
            if (!$property->isRelation()) {
522 4
                $generator->setNullable($forceNullable);
523 4
                $generator->setVarTag($property->className());
0 ignored issues
show
Bug introduced by
It seems like $property->className() can also be of type null; however, parameter $varTag of Bdf\Prime\Entity\PropertyGenerator::setVarTag() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

523
                $generator->setVarTag(/** @scrutinizer ignore-type */ $property->className());
Loading history...
524 4
                $generator->setInitialize('new '.$namespace->simplifyName($property->className()).'()');
0 ignored issues
show
Bug introduced by
It seems like $property->className() can also be of type null; however, parameter $name of Nette\PhpGenerator\PhpNamespace::simplifyName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

524
                $generator->setInitialize('new '.$namespace->simplifyName(/** @scrutinizer ignore-type */ $property->className()).'()');
Loading history...
525
526 4
                if ($this->useTypedProperties) {
527 3
                    $generator->setTypeHint($property->className());
0 ignored issues
show
Bug introduced by
It seems like $property->className() can also be of type null; however, parameter $typeHint of Bdf\Prime\Entity\PropertyGenerator::setTypeHint() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

527
                    $generator->setTypeHint(/** @scrutinizer ignore-type */ $property->className());
Loading history...
528
                }
529
530 4
                continue;
531
            }
532
533 25
            $generator->setNullable($nullable = $forceNullable || $this->useTypedProperties);
534
535 25
            switch (true) {
536 25
                case $property->isArray() && $property->wrapper() === null:
537
                    // Simple array relation
538 18
                    $generator->setDefaultValue([]);
539 18
                    $generator->setVarTag($property->className() . '[]');
540
541 18
                    if ($this->useTypedProperties) {
542 7
                        $generator->setTypeHint('array');
543
                    }
544 18
                    break;
545
546 25
                case $property->isArray() && $property->wrapper() !== null:
547
                    // Array relation with wrapper
548 1
                    $repository = $this->prime->repository($property->className());
549 1
                    $generator->setVarTag($repository->collectionFactory()->wrapperClass($property->wrapper()) . '|' . $property->className() . '[]');
0 ignored issues
show
Bug introduced by
It seems like $property->wrapper() can also be of type callable and null; however, parameter $wrapper of Bdf\Prime\Collection\Col...Factory::wrapperClass() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

549
                    $generator->setVarTag($repository->collectionFactory()->wrapperClass(/** @scrutinizer ignore-type */ $property->wrapper()) . '|' . $property->className() . '[]');
Loading history...
550
551
                    // The value is an object : so the default value must be null
552 1
                    if ($nullable) {
553 1
                        $generator->setDefaultValue(null);
554
                    }
555
556 1
                    if ($this->useTypedProperties) {
557 1
                        $generator->setTypeHint($repository->collectionFactory()->wrapperClass($property->wrapper()));
558
                    }
559
560
                    // @todo handle other wrapper types
561 1
                    if ($property->wrapper() === 'collection') {
562 1
                        $generator->setInitialize($namespace->simplifyName($property->className()).'::collection()');
563
                    }
564
565 1
                    break;
566
567
                default:
568
                    // Simple relation
569 25
                    $generator->setVarTag($property->className());
570 25
                    $generator->setInitialize('new '.$namespace->simplifyName($property->className()).'()');
571
572
                    // The value is an object : so the default value must be null
573 25
                    if ($nullable) {
574 12
                        $generator->setDefaultValue(null);
575
                    }
576
577 25
                    if ($this->useTypedProperties) {
578 12
                        $generator->setTypeHint($property->className());
579
                    }
580
            }
581
        }
582
583 35
        return $properties;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $properties returns the type Bdf\Prime\Entity\PropertyGenerator[]|array which is incompatible with the documented return type Bdf\Prime\Entity\list.
Loading history...
584
    }
585
586
    /**
587
     * Get accessor metadata for a given property
588
     *
589
     * @param PhpNamespace $namespace
590
     * @param InfoInterface $propertyInfo
591
     * @param string|null $prefix Accessor prefix. Can be null to use the field name as method name.
592
     * @param bool $one In case of array property, get metadata for single item instead of the whole array.
593
     *
594
     * @return array{method: string, variable: string, field: string, typeHint: string, docType: string, nullable: bool}|null Accessor metadata, or null if the method already exists.
595
     */
596 34
    protected function accessorMetadata(PhpNamespace $namespace, InfoInterface $propertyInfo, ?string $prefix, bool $one = false): ?array
597
    {
598 34
        $fieldName = $propertyInfo->name();
599
600 34
        if (!$prefix) {
601 33
            $variableName = $this->inflector->camelize($fieldName);
602 33
            $methodName = $variableName;
603
        } else {
604 34
            $methodName = $prefix . $this->inflector->classify($fieldName);
605 34
            $variableName = $this->inflector->camelize($fieldName);
606
        }
607
608 34
        if ($one) {
609 19
            $methodName = $this->inflector->singularize($methodName);
610 19
            $variableName = $this->inflector->singularize($variableName);
611
        }
612
613 34
        if ($this->hasMethod($methodName)) {
614 3
            return null;
615
        }
616
617 33
        $this->staticReflection[$this->mapperInfo->className()]['methods'][] = strtolower($methodName);
618
619 33
        if ($propertyInfo->isObject()) {
620
            /** @var ObjectPropertyInfo $propertyInfo */
621 25
            $variableType = $namespace->simplifyName($propertyInfo->className());
0 ignored issues
show
Bug introduced by
It seems like $propertyInfo->className() can also be of type null; however, parameter $name of Nette\PhpGenerator\PhpNamespace::simplifyName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

621
            $variableType = $namespace->simplifyName(/** @scrutinizer ignore-type */ $propertyInfo->className());
Loading history...
622
            // Only makes nullable for single relation
623 25
            $methodTypeHint = $propertyInfo->className();
624 25
            $nullable = (!$one && !$propertyInfo->isEmbedded());
625
        } else {
626
            /** @var PropertyInfo $propertyInfo */
627 33
            $variableType = $propertyInfo->phpType();
628 33
            $methodTypeHint = self::PROPERTY_TYPE_MAP[$variableType] ?? $variableType;
629 33
            $nullable = $propertyInfo->isNullable();
630
        }
631
632 33
        if ($propertyInfo->isArray() && $one === false) {
633 20
            if ($propertyInfo->isObject() && $propertyInfo->wrapper() !== null) {
0 ignored issues
show
Bug introduced by
The method wrapper() does not exist on Bdf\Prime\Mapper\Info\PropertyInfo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

633
            if ($propertyInfo->isObject() && $propertyInfo->/** @scrutinizer ignore-call */ wrapper() !== null) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
634
                /** @var ObjectPropertyInfo $propertyInfo */
635 1
                $repository = $this->prime->repository($propertyInfo->className());
636
637 1
                $methodTypeHint = $repository->collectionFactory()->wrapperClass($propertyInfo->wrapper());
0 ignored issues
show
Bug introduced by
It seems like $propertyInfo->wrapper() can also be of type callable; however, parameter $wrapper of Bdf\Prime\Collection\Col...Factory::wrapperClass() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

637
                $methodTypeHint = $repository->collectionFactory()->wrapperClass(/** @scrutinizer ignore-type */ $propertyInfo->wrapper());
Loading history...
638 1
                $variableType .= '[]|'.$namespace->simplifyName($methodTypeHint);
639
            } else {
640 19
                $methodTypeHint = 'array';
641
642 19
                if ($variableType !== 'array') {
643 19
                    $variableType .= '[]';
644
                }
645
            }
646
        }
647
648
        return [
649 33
            'field' => $fieldName,
650
            'variable' => $variableName,
651
            'method' => $methodName,
652
            'typeHint' => $methodTypeHint,
653
            'docType' => $variableType,
654
            'nullable' => $nullable,
655
        ];
656
    }
657
658 34
    protected function generateGetter(ClassType $generator, PhpNamespace $namespace, InfoInterface $propertyInfo): void
659
    {
660 34
        $metadata = $this->accessorMetadata($namespace, $propertyInfo, $this->useGetShortcutMethod ? null : 'get');
661
662 34
        if (!$metadata) {
663 3
            return;
664
        }
665
666 33
        $method = $generator->addMethod($metadata['method']);
667 33
        $method->addComment('Get ' . $metadata['variable']);
668 33
        $method->addComment('');
669 33
        $method->setReturnType($metadata['typeHint']);
670 33
        $method->setReturnNullable($metadata['nullable']);
671 33
        $method->setBody('return $this->?;', [$metadata['field']]);
672 33
        $method->addComment('@return ' . $metadata['docType']);
673
    }
674
675 34
    protected function generateSetter(ClassType $generator, PhpNamespace $namespace, InfoInterface $propertyInfo): void
676
    {
677 34
        $metadata = $this->accessorMetadata($namespace, $propertyInfo, 'set');
678
679 34
        if (!$metadata) {
680 3
            return;
681
        }
682
683 33
        $method = $generator->addMethod($metadata['method']);
684 33
        $method->addComment('Set ' . $metadata['variable']);
685 33
        $method->addComment('');
686 33
        $method->addComment('@param ' . $metadata['docType'] . ' $' . $metadata['variable']);
687 33
        $method->addComment('');
688 33
        $method->addComment('@return $this');
689 33
        $method->setReturnType('self');
690
        $method
691 33
            ->addParameter($metadata['variable'])
692 33
            ->setType($metadata['typeHint'])
693 33
            ->setNullable($metadata['nullable'])
694
        ;
695 33
        $method->addBody('$this->? = $?;', [$metadata['field'], $metadata['variable']]);
696 33
        $method->addBody('');
697 33
        $method->addBody('return $this;');
698
    }
699
700 19
    protected function generateAdder(ClassType $generator, PhpNamespace $namespace, InfoInterface $propertyInfo): void
701
    {
702 19
        $metadata = $this->accessorMetadata($namespace, $propertyInfo, 'add', true);
703
704 19
        if (!$metadata) {
705
            return;
706
        }
707
708 19
        $method = $generator->addMethod($metadata['method']);
709 19
        $method->addComment('Add ' . $metadata['variable']);
710 19
        $method->addComment('');
711 19
        $method->addComment('@param ' . $metadata['docType'] . ' $' . $metadata['variable']);
712 19
        $method->addComment('');
713 19
        $method->addComment('@return $this');
714 19
        $method->setReturnType('self');
715
        $method
716 19
            ->addParameter($metadata['variable'])
717 19
            ->setType($metadata['typeHint'])
718 19
            ->setNullable($metadata['nullable'])
719
        ;
720 19
        $method->addBody('$this->?[] = $?;', [$metadata['field'], $metadata['variable']]);
721 19
        $method->addBody('');
722 19
        $method->addBody('return $this;');
723
    }
724
725
    //
726
    //---------- tools methods
727
    //
728
729
    /**
730
     * @todo this won't work if there is a namespace in brackets and a class outside of it.
731
     *
732
     * @param string $src
733
     *
734
     * @return void
735
     */
736 4
    protected function parseTokensInEntityFile(string $src): void
737
    {
738 4
        $tokens = token_get_all($src);
739 4
        $lastSeenNamespace = "";
740
        /* @var class-string $lastSeenClass */
741 4
        $lastSeenClass = null;
742
743 4
        $inNamespace = false;
744 4
        $inClass = false;
745
746 4
        for ($i = 0; $i < count($tokens); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
747 4
            $token = $tokens[$i];
748 4
            if (in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT])) {
749 4
                continue;
750
            }
751
752 4
            if ($inNamespace) {
753 4
                if ($token[0] == T_NS_SEPARATOR || $token[0] == T_STRING || (defined('T_NAME_QUALIFIED') && $token[0] == T_NAME_QUALIFIED)) {
754 4
                    $lastSeenNamespace .= $token[1];
755 4
                } elseif (is_string($token) && in_array($token, [';', '{'])) {
756 4
                    $inNamespace = false;
757
                }
758
            }
759
760 4
            if ($inClass) {
761 4
                $inClass = false;
762 4
                $lastSeenClass = $lastSeenNamespace . ($lastSeenNamespace ? '\\' : '') . $token[1];
763 4
                $this->staticReflection[$lastSeenClass]['properties'] = [];
764 4
                $this->staticReflection[$lastSeenClass]['methods'] = [];
765
            }
766
767 4
            if ($token[0] == T_NAMESPACE) {
768 4
                $lastSeenNamespace = "";
769 4
                $inNamespace = true;
770 4
            } elseif ($token[0] == T_CLASS && $tokens[$i-1][0] != T_DOUBLE_COLON) {
771 4
                $inClass = true;
772 4
            } elseif ($token[0] == T_FUNCTION) {
773 3
                if ($tokens[$i+2][0] == T_STRING) {
774 3
                    $this->staticReflection[$lastSeenClass]['methods'][] = strtolower($tokens[$i+2][1]);
775
                } elseif ($tokens[$i+2] == "&" && $tokens[$i+3][0] == T_STRING) {
776 3
                    $this->staticReflection[$lastSeenClass]['methods'][] = strtolower($tokens[$i+3][1]);
777
                }
778 4
            } elseif (in_array($token[0], [T_VAR, T_PUBLIC, T_PRIVATE, T_PROTECTED]) && $tokens[$i+2][0] != T_FUNCTION) {
779 4
                $this->staticReflection[$lastSeenClass]['properties'][] = substr($tokens[$i+2][1], 1);
780
            }
781
        }
782
    }
783
784
    /**
785
     * @param string $property
786
     *
787
     * @return bool
788
     */
789 35
    protected function hasProperty(string $property): bool
790
    {
791 35
        if ($this->classToExtend) {
792
            // don't generate property if its already on the base class.
793 2
            $reflClass = new \ReflectionClass($this->getClassToExtend());
794 2
            if ($reflClass->hasProperty($property)) {
795 2
                return true;
796
            }
797
        }
798
799
        // check traits for existing property
800 35
        foreach ($this->getTraitsReflections() as $trait) {
801
            if ($trait->hasProperty($property)) {
802
                return true;
803
            }
804
        }
805
806
        return (
807 35
            isset($this->staticReflection[$this->mapperInfo->className()]) &&
808 35
            in_array($property, $this->staticReflection[$this->mapperInfo->className()]['properties'])
809
        );
810
    }
811
812
    /**
813
     * @param string $method
814
     *
815
     * @return bool
816
     */
817 35
    protected function hasMethod(string $method): bool
818
    {
819 35
        if ($this->classToExtend || (!$this->isNew && class_exists($this->mapperInfo->className()))) {
820
            // don't generate method if its already on the base class.
821 6
            $reflClass = new \ReflectionClass($this->getClassToExtend() ?: $this->mapperInfo->className());
822
823 6
            if ($reflClass->hasMethod($method)) {
824 6
                return true;
825
            }
826
        }
827
828
        // check traits for existing method
829 35
        foreach ($this->getTraitsReflections() as $trait) {
830
            if ($trait->hasMethod($method)) {
831
                return true;
832
            }
833
        }
834
835
        return (
836 35
            isset($this->staticReflection[$this->mapperInfo->className()]) &&
837 35
            in_array(strtolower($method), $this->staticReflection[$this->mapperInfo->className()]['methods'])
838
        );
839
    }
840
841
    /**
842
     * Get the class short name
843
     *
844
     * @param string $className
845
     *
846
     * @return string
847
     */
848 31
    protected function getClassName(string $className): string
849
    {
850 31
        $parts = explode('\\', $className);
851 31
        return array_pop($parts);
852
    }
853
854
    /**
855
     * @param string $className
856
     *
857
     * @return string
858
     */
859 35
    protected function getNamespace(string $className): string
860
    {
861 35
        $parts = explode('\\', $className);
862 35
        array_pop($parts);
863
864 35
        return implode('\\', $parts);
865
    }
866
867
    /**
868
     * @param string $className
869
     *
870
     * @return bool
871
     */
872 31
    protected function hasNamespace(string $className): bool
873
    {
874 31
        return strrpos($className, '\\') != 0;
875
    }
876
877
    /**
878
     * @return array<trait-string, \ReflectionClass>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<trait-string, \ReflectionClass> at position 2 could not be parsed: Unknown type name 'trait-string' at position 2 in array<trait-string, \ReflectionClass>.
Loading history...
879
     */
880 35
    protected function getTraitsReflections(): array
881
    {
882 35
        if ($this->isNew) {
883 31
            return [];
884
        }
885
886 4
        $reflClass = new \ReflectionClass($this->mapperInfo->className());
887
888 4
        $traits = [];
889
890 4
        while ($reflClass !== false) {
891 4
            $traits = array_merge($traits, $reflClass->getTraits());
892
893 4
            $reflClass = $reflClass->getParentClass();
894
        }
895
896 4
        return $traits;
897
    }
898
899
    //---------------------- mutators
900
901
    /**
902
     * Sets the number of spaces the exported class should have.
903
     *
904
     * @api
905
     */
906 2
    public function setNumSpaces(int $numSpaces): void
907
    {
908 2
        $this->numSpaces = $numSpaces;
909
    }
910
911
    /**
912
     * Gets the indentation spaces
913
     */
914 36
    public function getNumSpaces(): int
915
    {
916 36
        return $this->numSpaces;
917
    }
918
919
    /**
920
     * Sets the extension to use when writing php files to disk.
921
     *
922
     * @api
923
     */
924 1
    public function setExtension(string $extension): void
925
    {
926 1
        $this->extension = $extension;
927
    }
928
929
    /**
930
     * Get the file extension
931
     */
932 1
    public function getExtension(): string
933
    {
934 1
        return $this->extension;
935
    }
936
937
    /**
938
     * Sets the name of the class the generated classes should extend from.
939
     *
940
     * @api
941
     */
942 3
    public function setClassToExtend(string $classToExtend): void
943
    {
944 3
        $this->classToExtend = $classToExtend;
945
    }
946
947
    /**
948
     * Get the class to extend
949
     */
950 36
    public function getClassToExtend(): ?string
951
    {
952 36
        return $this->classToExtend;
953
    }
954
955
    /**
956
     * Add interface to implement
957
     *
958
     * @api
959
     *
960
     * @return void
961
     */
962 2
    public function addInterface(string $interface): void
963
    {
964 2
        $this->interfaces[$interface] = $interface;
965
    }
966
967
    /**
968
     * Sets the interfaces
969
     *
970
     * @param string[] $interfaces
971
     *
972
     * @api
973
     */
974 2
    public function setInterfaces(array $interfaces): void
975
    {
976 2
        $this->interfaces = $interfaces;
977
    }
978
979
    /**
980
     * Get the registered interfaces
981
     */
982 1
    public function getInterfaces(): array
983
    {
984 1
        return $this->interfaces;
985
    }
986
987
    /**
988
     * Add trait
989
     *
990
     * @api
991
     *
992
     * @return void
993
     */
994 2
    public function addTrait(string $trait): void
995
    {
996 2
        $this->traits[$trait] = $trait;
997
    }
998
999
    /**
1000
     * Sets the traits
1001
     *
1002
     * @param string[] $traits
1003
     *
1004
     * @api
1005
     */
1006 1
    public function setTraits(array $traits): void
1007
    {
1008 1
        $this->traits = $traits;
1009
    }
1010
1011
    /**
1012
     * Get the registered traits
1013
     */
1014 1
    public function getTraits(): array
1015
    {
1016 1
        return $this->traits;
1017
    }
1018
1019
    /**
1020
     * Sets the class fields visibility for the entity (can either be private or protected).
1021
     *
1022
     * @throws \InvalidArgumentException
1023
     *
1024
     * @api
1025
     */
1026 2
    public function setFieldVisibility(string $visibility): void
1027
    {
1028 2
        if ($visibility !== static::FIELD_VISIBLE_PRIVATE && $visibility !== static::FIELD_VISIBLE_PROTECTED) {
1029
            throw new \InvalidArgumentException('Invalid provided visibility (only private and protected are allowed): ' . $visibility);
1030
        }
1031
1032 2
        $this->fieldVisibility = $visibility;
0 ignored issues
show
Documentation Bug introduced by
It seems like $visibility of type string is incompatible with the declared type Bdf\Prime\Entity\EntityGenerator of property $fieldVisibility.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1033
    }
1034
1035
    /**
1036
     * Get the field visibility
1037
     */
1038 1
    public function getFieldVisibility(): string
1039
    {
1040 1
        return $this->fieldVisibility;
1041
    }
1042
1043
    /**
1044
     * Sets whether or not to try and update the entity if it already exists.
1045
     *
1046
     * @api
1047
     */
1048 5
    public function setUpdateEntityIfExists(bool $bool): void
1049
    {
1050 5
        $this->updateEntityIfExists = $bool;
1051
    }
1052
1053
    /**
1054
     * Get the flag for updating the entity
1055
     */
1056 1
    public function getUpdateEntityIfExists(): bool
1057
    {
1058 1
        return $this->updateEntityIfExists;
1059
    }
1060
1061
    /**
1062
     * Sets whether or not to regenerate the entity if it exists.
1063
     *
1064
     * @api
1065
     */
1066 1
    public function setRegenerateEntityIfExists(bool $bool): void
1067
    {
1068 1
        $this->regenerateEntityIfExists = $bool;
1069
    }
1070
1071
    /**
1072
     * Get the flag for regenerating entity
1073
     */
1074 1
    public function getRegenerateEntityIfExists(): bool
1075
    {
1076 1
        return $this->regenerateEntityIfExists;
1077
    }
1078
1079
    /**
1080
     * Sets whether or not to generate stub methods for the entity.
1081
     *
1082
     * @api
1083
     */
1084 2
    public function setGenerateStubMethods(bool $bool): void
1085
    {
1086 2
        $this->generateEntityStubMethods = $bool;
1087
    }
1088
1089
    /**
1090
     * Get the flag for generating stub methods
1091
     */
1092 1
    public function getGenerateStubMethods(): bool
1093
    {
1094 1
        return $this->generateEntityStubMethods;
1095
    }
1096
1097
    /**
1098
     * Sets whether or not the get mehtod will be suffixed by 'get'.
1099
     *
1100
     * @param bool $flag
1101
     *
1102
     * @return void
1103
     *
1104
     * @api
1105
     */
1106 2
    public function useGetShortcutMethod(bool $flag = true)
1107
    {
1108 2
        $this->useGetShortcutMethod = $flag;
1109
    }
1110
1111
    /**
1112
     * Get the flag for get mehtod name.
1113
     */
1114 1
    public function getUseGetShortcutMethod(): bool
1115
    {
1116 1
        return $this->useGetShortcutMethod;
1117
    }
1118
1119
    /**
1120
     * @return bool
1121
     */
1122
    public function getUseTypedProperties(): bool
1123
    {
1124
        return $this->useTypedProperties;
1125
    }
1126
1127
    /**
1128
     * Enable usage of php 7.4 type properties
1129
     *
1130
     * @param bool $useTypedProperties
1131
     */
1132 15
    public function useTypedProperties(bool $useTypedProperties = true): void
1133
    {
1134 15
        $this->useTypedProperties = $useTypedProperties;
1135
    }
1136
1137
    /**
1138
     * @return bool
1139
     */
1140
    public function getUseConstructorPropertyPromotion(): bool
1141
    {
1142
        return $this->useConstructorPropertyPromotion;
1143
    }
1144
1145
    /**
1146
     * Enable usage of PHP 8 promoted properties on constructor instead of array import
1147
     *
1148
     * @param bool $useConstructorPropertyPromotion
1149
     */
1150 2
    public function useConstructorPropertyPromotion(bool $useConstructorPropertyPromotion = true): void
1151
    {
1152 2
        $this->useConstructorPropertyPromotion = $useConstructorPropertyPromotion;
1153
    }
1154
}
1155
1156
/**
1157
 * @internal
1158
 */
1159
class PropertyGenerator
1160
{
1161
    private string $name;
1162
    private ?string $typeHint = null;
1163
    private bool $nullable = false;
1164
    private ?string $varTag = null;
1165
    private string $visibility = EntityGenerator::FIELD_VISIBLE_PROTECTED;
1166
    private $defaultValue;
1167
    private bool $hasDefaultValue = false;
1168
    private ?string $initialize = null;
1169
1170
    /**
1171
     * @param string $name
1172
     */
1173 33
    public function __construct(string $name)
1174
    {
1175 33
        $this->name = $name;
1176
    }
1177
1178 15
    public function setTypeHint(string $typeHint): void
1179
    {
1180 15
        $this->typeHint = $typeHint;
1181
    }
1182
1183 33
    public function setNullable(bool $nullable): void
1184
    {
1185 33
        $this->nullable = $nullable;
1186
    }
1187
1188 33
    public function setVarTag(string $varTag): void
1189
    {
1190 33
        $this->varTag = $varTag;
1191
    }
1192
1193 33
    public function setVisibility(string $visibility): void
1194
    {
1195 33
        $this->visibility = $visibility;
1196
    }
1197
1198 29
    public function setDefaultValue($value): void
1199
    {
1200 29
        $this->defaultValue = $value;
1201 29
        $this->hasDefaultValue = true;
1202
    }
1203
1204 29
    public function setInitialize(string $initialize): void
1205
    {
1206 29
        $this->initialize = $initialize;
1207
    }
1208
1209 25
    public function hasInitialisation(): bool
1210
    {
1211 25
        return $this->initialize !== null;
1212
    }
1213
1214 31
    public function addProperty(ClassType $generator, ?PhpNamespace $namespace): void
1215
    {
1216 31
        $property = $generator->addProperty($this->name);
1217
1218
        $property
1219 31
            ->setNullable($this->nullable)
1220 31
            ->setVisibility($this->visibility)
1221
        ;
1222
1223 31
        if ($this->typeHint) {
1224 13
            $typehint = EntityGenerator::PROPERTY_TYPE_MAP[$this->typeHint] ?? $this->typeHint;
1225 13
            $property->setType($typehint);
1226
        }
1227
1228 31
        if ($this->hasDefaultValue) {
1229 27
            $property->setValue($this->defaultValue);
1230
        }
1231
1232 31
        if ($this->varTag) {
1233 31
            $type = $this->varTag;
1234
1235 31
            if (!isset(EntityGenerator::PROPERTY_TYPE_MAP[$this->varTag]) && $namespace) {
1236 31
                $type = $this->simplifyType($type, $namespace);
1237
            }
1238
1239 31
            $property->addComment("\n@var $type");
1240
        }
1241
    }
1242
1243 2
    public function addPromotedProperty(Method $constructor, ?PhpNamespace $namespace): void
1244
    {
1245 2
        $parameter = $constructor->addPromotedParameter($this->name);
1246
1247
        $parameter
1248 2
            ->setNullable($this->nullable)
1249 2
            ->setVisibility($this->visibility)
1250
        ;
1251
1252 2
        if ($this->typeHint) {
1253 2
            $typehint = EntityGenerator::PROPERTY_TYPE_MAP[$this->typeHint] ?? $this->typeHint;
1254 2
            $parameter->setType($typehint);
1255
        }
1256
1257 2
        if ($this->hasDefaultValue) {
1258 2
            $parameter->setDefaultValue($this->defaultValue);
1259
        }
1260
1261 2
        if ($this->varTag) {
1262 2
            $type = $this->varTag;
1263
1264 2
            if (!isset(EntityGenerator::PROPERTY_TYPE_MAP[$this->varTag]) && $namespace) {
1265 2
                $type = $this->simplifyType($type, $namespace);
1266
            }
1267
1268 2
            $parameter->addComment("\n@var $type");
1269
        }
1270
    }
1271
1272 27
    public function addInitializeLine(Method $initializeMethod, string $assignationOperator = '='): void
1273
    {
1274 27
        if ($this->initialize) {
1275 27
            $initializeMethod->addBody('$this->'.$this->name.' '.$assignationOperator.' '.$this->initialize.';');
1276
        }
1277
    }
1278
1279 33
    private function simplifyType(string $type, PhpNamespace $namespace): string
1280
    {
1281 33
        $types = explode('|', $type);
1282
1283 33
        foreach ($types as &$part) {
1284 33
            $atomicType = $part;
1285 33
            $isArray = false;
1286
1287 33
            if (str_ends_with($atomicType, '[]')) {
1288 19
                $atomicType = substr($atomicType, 0, -2);
1289 19
                $isArray = true;
1290
            }
1291
1292 33
            if (isset(EntityGenerator::PROPERTY_TYPE_MAP[$atomicType])) {
1293
                continue;
1294
            }
1295
1296 33
            $part = $namespace->simplifyName($atomicType) . ($isArray ? '[]' : '');
1297
        }
1298
1299 33
        return implode('|', $types);
1300
    }
1301
}
1302
1303
/**
1304
 * @internal
1305
 */
1306
class ConfigurableEntityPrinter extends Printer
1307
{
1308 35
    public function __construct(EntityGenerator $generator)
1309
    {
1310 35
        parent::__construct();
1311
1312 35
        $this->linesBetweenMethods = 1;
1313 35
        $this->linesBetweenProperties = 1;
1314 35
        $this->indentation = str_repeat(' ', $generator->getNumSpaces());
1315
    }
1316
1317 35
    public function printClass($class, ?PhpNamespace $namespace = null): string
1318
    {
1319 35
        $code = parent::printClass($class, $namespace);
1320
1321
        // Reformat property docblock : nette will generate property doc on single line
1322 35
        return preg_replace('#^( *)/\*\*(.*)\s+\*/$#m', "$1/**\n$1 *$2\n$1 */", $code);
1323
    }
1324
}
1325