Passed
Push — chore-merge-20 ( ee0370 )
by Vincent
07:01 queued 13s
created

generateEntityEmbeddedProperties()   D

Complexity

Conditions 18
Paths 20

Size

Total Lines 82
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 18

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 41
c 1
b 0
f 0
dl 0
loc 82
ccs 41
cts 41
cp 1
rs 4.8666
cc 18
nc 20
nop 2
crap 18

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

279
        $class->addComment(/** @scrutinizer ignore-type */ $class->getName());
Loading history...
280
281 32
        if ($this->classToExtend) {
282 2
            $class->setExtends($this->classToExtend);
283
        }
284
285 32
        foreach ($this->interfaces as $interface) {
286 3
            $class->addImplement($interface);
287
        }
288
289 32
        $class->setTraits($this->traits);
290
    }
291
292
    /**
293
     * Generate use part
294
     */
295 32
    protected function generateEntityUse(EntityClassGenerator $generator): void
296
    {
297 32
        if (($parentClass = $this->getClassToExtend())) {
298 2
            $generator->addUse($parentClass);
299
        }
300
301 32
        foreach ($this->interfaces as $interface) {
302 3
            $generator->addUse($interface);
303
        }
304
305 32
        foreach ($this->traits as $trait) {
306 2
            $generator->addUse($trait);
307
        }
308
309 32
        foreach ($this->mapperInfo->objects() as $info) {
310 27
            $className = $info->className();
311 27
            if (!$info->belongsToRoot()) {
312 5
                continue;
313
            }
314
315 27
            $generator->addUse($className);
0 ignored issues
show
Bug introduced by
It seems like $className can also be of type null; however, parameter $class of Bdf\Prime\Entity\EntityClassGenerator::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

315
            $generator->addUse(/** @scrutinizer ignore-type */ $className);
Loading history...
316
317 27
            if ($info->wrapper() !== null) {
318 1
                $repository = $this->prime->repository($className);
319 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

319
                $wrapperClass = $repository->collectionFactory()->wrapperClass(/** @scrutinizer ignore-type */ $info->wrapper());
Loading history...
320
321 1
                $generator->addUse($wrapperClass);
322
            }
323
        }
324
    }
325
326 38
    protected function generateEntityBody(EntityClassGenerator $generator): void
327
    {
328 38
        $properties = [
329 38
            ...$this->generateEntityFieldMappingProperties($generator, $this->useConstructorPropertyPromotion),
330 38
            ...$this->generateEntityEmbeddedProperties($generator, $this->useConstructorPropertyPromotion)
331 38
        ];
332
333 38
        if (!$this->useConstructorPropertyPromotion) {
334 33
            foreach ($properties as $property) {
335 31
                $property->addProperty($generator);
336
            }
337
        }
338
339 38
        if ($this->generateEntityStubMethods) {
340 37
            $this->generateEntityStubMethods($generator);
341
        }
342
343 38
        $this->generateEntityConstructor($generator, $this->useConstructorPropertyPromotion, $properties);
344
    }
345
346
    /**
347
     * @param bool $propertyPromotion Generate constructor with property promotion
348
     * @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...
349
     */
350 38
    protected function generateEntityConstructor(EntityClassGenerator $generator, bool $propertyPromotion, array $properties): void
351
    {
352 38
        $initializable = in_array(InitializableInterface::class, $this->interfaces);
353 38
        $isImportable  = in_array(ImportableInterface::class, $this->interfaces)
354 38
                    || is_subclass_of($this->classToExtend, ImportableInterface::class);
355
356 38
        if ($propertyPromotion) {
357 5
            $this->generateConstructorWithPromotedProperties($generator, $initializable, $properties);
358
        } else {
359 33
            $this->generateClassicConstructor($generator, $isImportable, $initializable, $properties);
360
        }
361
362 38
        if (!$generator->hasMethod('initialize') && $initializable) {
363 1
            $init = Method::from([InitializableInterface::class, 'initialize'])
364 1
                ->addComment('{@inheritdoc}');
365
366 1
            foreach ($properties as $property) {
367 1
                $property->addInitializeLine($init);
368
            }
369
370 1
            $generator->addMember($init);
371
        }
372
    }
373
374
    /**
375
     * Generate PHP 8 constructor
376
     *
377
     * @param bool $initializable Does the entity class implements InitializableInterface ?
378
     * @param list<PropertyGenerator> $properties Properties to declare an initialize
379
     */
380 5
    private function generateConstructorWithPromotedProperties(EntityClassGenerator $generator, bool $initializable, array $properties): void
381
    {
382 5
        $isUpdate = $generator->hasMethod('__construct');
383 5
        $constructor = $isUpdate ? $generator->getMethod('__construct') : $generator->addMethod('__construct');
384
385 5
        foreach ($properties as $property) {
386 4
            $property->addPromotedProperty($constructor, $generator);
387
        }
388
389
        // Only declare new properties
390 5
        if ($isUpdate) {
391 2
            return;
392
        }
393
394 3
        if ($initializable) {
395
            $constructor->addBody('$this->initialize();');
396
        } else {
397 3
            foreach ($properties as $property) {
398
                // Assignment operator : use null coalesce assignment with property promotion
399
                // because assignation is performed before initializing default value
400 3
                $property->addInitializeLine($constructor, '??=');
401
            }
402
        }
403
    }
404
405
    /**
406
     * Generate classic constructor
407
     *
408
     * @param bool $isImportable Does the entity class implements InitializableInterface ?
409
     * @param bool $initializable Does the entity class implements ImportableInterface ?
410
     * @param list<PropertyGenerator> $properties Properties to initialize
411
     */
412 33
    private function generateClassicConstructor(EntityClassGenerator $generator, bool $isImportable, bool $initializable, array $properties): void
413
    {
414
        // Do not support constructor update
415 33
        if ($generator->hasMethod('__construct')) {
416 2
            return;
417
        }
418
419 31
        if ($isImportable) {
420 2
            $constructor = $generator->addMethod('__construct');
421
422 2
            $constructor
423 2
                ->addParameter('data', [])
424 2
                ->setType('array')
425 2
            ;
426
427 2
            if ($initializable) {
428 1
                $constructor->addBody('$this->initialize();');
429
            } else {
430 1
                foreach ($properties as $property) {
431 1
                    $property->addInitializeLine($constructor);
432
                }
433
            }
434
435 2
            $constructor->addBody('$this->import($data);');
436 29
        } elseif (!$initializable) {
437 29
            $constructor = null;
438
439 29
            foreach ($properties as $property) {
440 27
                if ($property->hasInitialisation()) {
441
                    // Add a constructor only if it's necessary
442 23
                    $constructor = $constructor ?? $generator->addMethod('__construct');
443 23
                    $property->addInitializeLine($constructor);
444
                }
445
            }
446
        }
447
    }
448
449 37
    protected function generateEntityStubMethods(EntityClassGenerator $generator): void
450
    {
451 37
        foreach ($this->mapperInfo->properties() as $property) {
452 37
            $this->generateSetter($generator, $property);
453 37
            $this->generateGetter($generator, $property);
454
        }
455
456 37
        foreach ($this->mapperInfo->objects() as $property) {
457 26
            if (!$property->belongsToRoot()) {
458 5
                continue;
459
            }
460
461 26
            if ($property->isArray() && $property->wrapper() === null) {
462 19
                $this->generateAdder($generator, $property);
463
            }
464
465 26
            $this->generateSetter($generator, $property);
466 26
            $this->generateGetter($generator, $property);
467
        }
468
    }
469
470
    /**
471
     * @param bool $forceNullable Force typehint to be nullable. Useful property promotion
472
     * @return list<PropertyGenerator>
473
     */
474 38
    protected function generateEntityFieldMappingProperties(EntityClassGenerator $class, bool $forceNullable = false): array
475
    {
476 38
        $properties = [];
477
478 38
        foreach ($this->mapperInfo->properties() as $property) {
479 38
            if ($class->hasProperty($property->name())) {
480 8
                continue;
481
            }
482
483 35
            $properties[] = $generator = new PropertyGenerator($property->name());
484
485 35
            $generator->setNullable($forceNullable || $property->isNullable());
486 35
            $generator->setVisibility($this->fieldVisibility);
487 35
            $generator->setVarTag($property->phpType());
488
489 35
            if ($this->useTypedProperties) {
490 17
                $generator->setTypeHint($property->phpType());
491
            }
492
493 35
            if ($property->hasDefault() && !$property->isDateTime()) {
494 5
                $generator->setDefaultValue($property->convert($property->getDefault()));
495 35
            } elseif ($property->isArray()) {
496 17
                $generator->setDefaultValue([]);
497 33
            } elseif (($forceNullable || ($this->useTypedProperties && $property->isNullable()))) {
498
                // A nullable property should be defined as null by default
499
                // A property is considered as nullable if it's explicitly defined on mapper or if the field is auto-generated
500 15
                $generator->setDefaultValue(null);
501
            }
502
503 35
            if ($property->hasDefault() && $property->isDateTime()) {
504 4
                $constructorArgs = '';
505
                // Add the default timezone from the property type.
506 4
                if ($timezone = $property->getTimezone()) {
507 4
                    $constructorArgs = "'now', new \DateTimeZone('$timezone')";
508
                }
509
510 4
                $generator->setInitialize('new '.$property->phpType().'('.$constructorArgs.')');
511
            }
512
        }
513
514 38
        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...
515
    }
516
517
    /**
518
     * @param bool $forceNullable Force typehint to be nullable. Useful property promotion
519
     * @return list<PropertyGenerator>
520
     */
521 38
    protected function generateEntityEmbeddedProperties(EntityClassGenerator $class, bool $forceNullable = false): array
522
    {
523 38
        $properties = [];
524
525 38
        foreach ($this->mapperInfo->objects() as $property) {
526 27
            if (!$property->belongsToRoot() || $class->hasProperty($property->name())) {
527 7
                continue;
528
            }
529
530 27
            $properties[] = $generator = new PropertyGenerator($property->name());
531 27
            $generator->setVisibility($this->fieldVisibility);
532
533
            // Embedded property : should not be null
534 27
            if (!$property->isRelation()) {
535 5
                $generator->setNullable($forceNullable);
536
537
                // Always add a default value with use property promotion
538 5
                if ($this->useConstructorPropertyPromotion) {
539 1
                    $generator->setDefaultValue(null);
540
                }
541
542 5
                $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

542
                $generator->setVarTag(/** @scrutinizer ignore-type */ $property->className());
Loading history...
543 5
                $generator->setInitialize('new '.$class->simplifyType($property->className()).'()');
0 ignored issues
show
Bug introduced by
It seems like $property->className() can also be of type null; however, parameter $type of Bdf\Prime\Entity\EntityC...nerator::simplifyType() 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

543
                $generator->setInitialize('new '.$class->simplifyType(/** @scrutinizer ignore-type */ $property->className()).'()');
Loading history...
544
545 5
                if ($this->useTypedProperties) {
546 4
                    $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

546
                    $generator->setTypeHint(/** @scrutinizer ignore-type */ $property->className());
Loading history...
547
                }
548
549 5
                continue;
550
            }
551
552 26
            $generator->setNullable($nullable = $forceNullable || $this->useTypedProperties);
553
554 26
            switch (true) {
555 26
                case $property->isArray() && $property->wrapper() === null:
556
                    // Simple array relation
557 18
                    $generator->setDefaultValue([]);
558 18
                    $generator->setVarTag($property->className() . '[]');
559
560 18
                    if ($this->useTypedProperties) {
561 7
                        $generator->setTypeHint('array');
562
                    }
563 18
                    break;
564
565 26
                case $property->isArray() && $property->wrapper() !== null:
566
                    // Array relation with wrapper
567 1
                    $repository = $this->prime->repository($property->className());
568 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

568
                    $generator->setVarTag($repository->collectionFactory()->wrapperClass(/** @scrutinizer ignore-type */ $property->wrapper()) . '|' . $property->className() . '[]');
Loading history...
569
570
                    // The value is an object : so the default value must be null
571 1
                    if ($nullable) {
572 1
                        $generator->setDefaultValue(null);
573
                    }
574
575 1
                    if ($this->useTypedProperties) {
576 1
                        $generator->setTypeHint($repository->collectionFactory()->wrapperClass($property->wrapper()));
577
                    }
578
579
                    // @todo handle other wrapper types
580 1
                    if ($property->wrapper() === 'collection') {
581 1
                        $generator->setInitialize($class->simplifyType($property->className()).'::collection()');
582
                    }
583
584 1
                    break;
585
586
                default:
587
                    // Simple relation
588 26
                    $generator->setVarTag($property->className());
589 26
                    $generator->setInitialize('new '.$class->simplifyType($property->className()).'()');
590
591
                    // The value is an object : so the default value must be null
592 26
                    if ($nullable) {
593 13
                        $generator->setDefaultValue(null);
594
                    }
595
596 26
                    if ($this->useTypedProperties) {
597 13
                        $generator->setTypeHint($property->className());
598
                    }
599
            }
600
        }
601
602 38
        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...
603
    }
604
605
    /**
606
     * Get accessor metadata for a given property
607
     *
608
     * @param EntityClassGenerator $generator
609
     * @param InfoInterface $propertyInfo
610
     * @param string|null $prefix Accessor prefix. Can be null to use the field name as method name.
611
     * @param bool $one In case of array property, get metadata for single item instead of the whole array.
612
     *
613
     * @return array{method: string, variable: string, field: string, typeHint: string, docType: string|null, nullable: bool}|null Accessor metadata, or null if the method already exists.
614
     */
615 37
    protected function accessorMetadata(EntityClassGenerator $generator, InfoInterface $propertyInfo, ?string $prefix, bool $one = false): ?array
616
    {
617 37
        $fieldName = $propertyInfo->name();
618
619 37
        if (!$prefix) {
620 36
            $variableName = $this->inflector->camelize($fieldName);
621 36
            $methodName = $variableName;
622
        } else {
623 37
            $methodName = $prefix . $this->inflector->classify($fieldName);
624 37
            $variableName = $this->inflector->camelize($fieldName);
625
        }
626
627 37
        if ($one) {
628 19
            $methodName = $this->inflector->singularize($methodName);
629 19
            $variableName = $this->inflector->singularize($variableName);
630
        }
631
632 37
        if ($generator->hasMethod($methodName)) {
633 5
            return null;
634
        }
635
636 35
        $variableType = null;
637
638 35
        if ($propertyInfo->isObject()) {
639
            /** @var ObjectPropertyInfo $propertyInfo */
640
            // Only makes nullable for single relation
641 26
            $methodTypeHint = $propertyInfo->className();
642 26
            $nullable = (!$one && !$propertyInfo->isEmbedded());
643
        } else {
644
            /** @var PropertyInfo $propertyInfo */
645 35
            $methodTypeHint = self::PROPERTY_TYPE_MAP[$propertyInfo->phpType()] ?? $propertyInfo->phpType();
646 35
            $nullable = $propertyInfo->isNullable();
647
        }
648
649 35
        if ($propertyInfo->isArray() && $one === false) {
650 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

650
            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...
651
                /** @var ObjectPropertyInfo $propertyInfo */
652 1
                $repository = $this->prime->repository($propertyInfo->className());
653
654 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

654
                $methodTypeHint = $repository->collectionFactory()->wrapperClass(/** @scrutinizer ignore-type */ $propertyInfo->wrapper());
Loading history...
655 1
                $variableType = $generator->simplifyType($propertyInfo->className()) . '[]|'.$generator->simplifyType($methodTypeHint);
0 ignored issues
show
Bug introduced by
It seems like $propertyInfo->className() can also be of type null; however, parameter $type of Bdf\Prime\Entity\EntityC...nerator::simplifyType() 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

655
                $variableType = $generator->simplifyType(/** @scrutinizer ignore-type */ $propertyInfo->className()) . '[]|'.$generator->simplifyType($methodTypeHint);
Loading history...
656
            } else {
657 19
                $methodTypeHint = 'array';
658 19
                $variableType = $propertyInfo->isObject() ? $propertyInfo->className() : $propertyInfo->phpType();
0 ignored issues
show
Bug introduced by
The method phpType() does not exist on Bdf\Prime\Mapper\Info\ObjectPropertyInfo. ( Ignorable by Annotation )

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

658
                $variableType = $propertyInfo->isObject() ? $propertyInfo->className() : $propertyInfo->/** @scrutinizer ignore-call */ phpType();

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...
Bug introduced by
The method className() 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

658
                $variableType = $propertyInfo->isObject() ? $propertyInfo->/** @scrutinizer ignore-call */ className() : $propertyInfo->phpType();

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...
659
660 19
                if ($variableType !== 'array') {
661 19
                    $variableType .= '[]';
662
                }
663
            }
664
        }
665
666 35
        return [
667 35
            'field' => $fieldName,
668 35
            'variable' => $variableName,
669 35
            'method' => $methodName,
670 35
            'typeHint' => $methodTypeHint,
671 35
            'docType' => $variableType,
672 35
            'nullable' => $nullable,
673 35
        ];
674
    }
675
676 37
    protected function generateGetter(EntityClassGenerator $generator, InfoInterface $propertyInfo): void
677
    {
678 37
        $metadata = $this->accessorMetadata($generator, $propertyInfo, $this->useGetShortcutMethod ? null : 'get');
679
680 37
        if (!$metadata) {
681 5
            return;
682
        }
683
684 35
        $method = $generator->addMethod($metadata['method']);
685 35
        $method->addComment('Get ' . $metadata['variable']);
686 35
        $method->addComment('');
687 35
        $method->setReturnType($metadata['typeHint']);
688 35
        $method->setReturnNullable($metadata['nullable']);
689 35
        $method->setBody('return $this->?;', [$metadata['field']]);
690
691 35
        if ($metadata['docType']) {
692 20
            $method->addComment('@return ' . $metadata['docType']);
693
        }
694
    }
695
696 37
    protected function generateSetter(EntityClassGenerator $generator, InfoInterface $propertyInfo): void
697
    {
698 37
        $metadata = $this->accessorMetadata($generator, $propertyInfo, 'set');
699
700 37
        if (!$metadata) {
701 5
            return;
702
        }
703
704 35
        $method = $generator->addMethod($metadata['method']);
705 35
        $method->addComment('Set ' . $metadata['variable']);
706 35
        $method->addComment('');
707
708 35
        if ($metadata['docType']) {
709 20
            $method->addComment('@param ' . $metadata['docType'] . ' $' . $metadata['variable']);
710 20
            $method->addComment('');
711
        }
712
713 35
        $method->addComment('@return $this');
714 35
        $method->setReturnType('self');
715 35
        $method
716 35
            ->addParameter($metadata['variable'])
717 35
            ->setType($metadata['typeHint'])
718 35
            ->setNullable($metadata['nullable'])
719 35
        ;
720 35
        $method->addBody('$this->? = $?;', [$metadata['field'], $metadata['variable']]);
721 35
        $method->addBody('');
722 35
        $method->addBody('return $this;');
723
    }
724
725 19
    protected function generateAdder(EntityClassGenerator $generator, InfoInterface $propertyInfo): void
726
    {
727 19
        $metadata = $this->accessorMetadata($generator, $propertyInfo, 'add', true);
728
729 19
        if (!$metadata) {
730
            return;
731
        }
732
733 19
        $method = $generator->addMethod($metadata['method']);
734 19
        $method->addComment('Add ' . $metadata['variable']);
735 19
        $method->addComment('');
736
737 19
        if ($metadata['docType']) {
738
            $method->addComment('@param ' . $metadata['docType'] . ' $' . $metadata['variable']);
739
            $method->addComment('');
740
        }
741
742 19
        $method->addComment('@return $this');
743 19
        $method->setReturnType('self');
744 19
        $method
745 19
            ->addParameter($metadata['variable'])
746 19
            ->setType($metadata['typeHint'])
747 19
            ->setNullable($metadata['nullable'])
748 19
        ;
749 19
        $method->addBody('$this->?[] = $?;', [$metadata['field'], $metadata['variable']]);
750 19
        $method->addBody('');
751 19
        $method->addBody('return $this;');
752
    }
753
754
    //---------------------- mutators
755
756
    /**
757
     * Sets the number of spaces the exported class should have.
758
     *
759
     * @api
760
     */
761 2
    public function setNumSpaces(int $numSpaces): void
762
    {
763 2
        $this->numSpaces = $numSpaces;
764
    }
765
766
    /**
767
     * Gets the indentation spaces
768
     */
769 39
    public function getNumSpaces(): int
770
    {
771 39
        return $this->numSpaces;
772
    }
773
774
    /**
775
     * Sets the extension to use when writing php files to disk.
776
     *
777
     * @api
778
     */
779 1
    public function setExtension(string $extension): void
780
    {
781 1
        $this->extension = $extension;
782
    }
783
784
    /**
785
     * Get the file extension
786
     */
787 1
    public function getExtension(): string
788
    {
789 1
        return $this->extension;
790
    }
791
792
    /**
793
     * Sets the name of the class the generated classes should extend from.
794
     *
795
     * @api
796
     */
797 3
    public function setClassToExtend(string $classToExtend): void
798
    {
799 3
        $this->classToExtend = $classToExtend;
800
    }
801
802
    /**
803
     * Get the class to extend
804
     */
805 33
    public function getClassToExtend(): ?string
806
    {
807 33
        return $this->classToExtend;
808
    }
809
810
    /**
811
     * Add interface to implement
812
     *
813
     * @api
814
     *
815
     * @return void
816
     */
817 2
    public function addInterface(string $interface): void
818
    {
819 2
        $this->interfaces[$interface] = $interface;
820
    }
821
822
    /**
823
     * Sets the interfaces
824
     *
825
     * @param string[] $interfaces
826
     *
827
     * @api
828
     */
829 2
    public function setInterfaces(array $interfaces): void
830
    {
831 2
        $this->interfaces = $interfaces;
832
    }
833
834
    /**
835
     * Get the registered interfaces
836
     */
837 1
    public function getInterfaces(): array
838
    {
839 1
        return $this->interfaces;
840
    }
841
842
    /**
843
     * Add trait
844
     *
845
     * @api
846
     *
847
     * @return void
848
     */
849 2
    public function addTrait(string $trait): void
850
    {
851 2
        $this->traits[$trait] = $trait;
852
    }
853
854
    /**
855
     * Sets the traits
856
     *
857
     * @param string[] $traits
858
     *
859
     * @api
860
     */
861 1
    public function setTraits(array $traits): void
862
    {
863 1
        $this->traits = $traits;
864
    }
865
866
    /**
867
     * Get the registered traits
868
     */
869 1
    public function getTraits(): array
870
    {
871 1
        return $this->traits;
872
    }
873
874
    /**
875
     * Sets the class fields visibility for the entity (can either be private or protected).
876
     *
877
     * @throws \InvalidArgumentException
878
     *
879
     * @api
880
     */
881 2
    public function setFieldVisibility(string $visibility): void
882
    {
883 2
        if ($visibility !== static::FIELD_VISIBLE_PRIVATE && $visibility !== static::FIELD_VISIBLE_PROTECTED) {
884
            throw new \InvalidArgumentException('Invalid provided visibility (only private and protected are allowed): ' . $visibility);
885
        }
886
887 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...
888
    }
889
890
    /**
891
     * Get the field visibility
892
     */
893 1
    public function getFieldVisibility(): string
894
    {
895 1
        return $this->fieldVisibility;
896
    }
897
898
    /**
899
     * Sets whether or not to try and update the entity if it already exists.
900
     *
901
     * @api
902
     */
903 7
    public function setUpdateEntityIfExists(bool $bool): void
904
    {
905 7
        $this->updateEntityIfExists = $bool;
906
    }
907
908
    /**
909
     * Get the flag for updating the entity
910
     */
911 1
    public function getUpdateEntityIfExists(): bool
912
    {
913 1
        return $this->updateEntityIfExists;
914
    }
915
916
    /**
917
     * Sets whether or not to regenerate the entity if it exists.
918
     *
919
     * @api
920
     */
921 1
    public function setRegenerateEntityIfExists(bool $bool): void
922
    {
923 1
        $this->regenerateEntityIfExists = $bool;
924
    }
925
926
    /**
927
     * Get the flag for regenerating entity
928
     */
929 1
    public function getRegenerateEntityIfExists(): bool
930
    {
931 1
        return $this->regenerateEntityIfExists;
932
    }
933
934
    /**
935
     * Sets whether or not to generate stub methods for the entity.
936
     *
937
     * @api
938
     */
939 2
    public function setGenerateStubMethods(bool $bool): void
940
    {
941 2
        $this->generateEntityStubMethods = $bool;
942
    }
943
944
    /**
945
     * Get the flag for generating stub methods
946
     */
947 1
    public function getGenerateStubMethods(): bool
948
    {
949 1
        return $this->generateEntityStubMethods;
950
    }
951
952
    /**
953
     * Sets whether or not the get mehtod will be suffixed by 'get'.
954
     *
955
     * @param bool $flag
956
     *
957
     * @return void
958
     *
959
     * @api
960
     */
961 2
    public function useGetShortcutMethod(bool $flag = true)
962
    {
963 2
        $this->useGetShortcutMethod = $flag;
964
    }
965
966
    /**
967
     * Get the flag for get mehtod name.
968
     */
969 1
    public function getUseGetShortcutMethod(): bool
970
    {
971 1
        return $this->useGetShortcutMethod;
972
    }
973
974
    /**
975
     * @return bool
976
     */
977
    public function getUseTypedProperties(): bool
978
    {
979
        return $this->useTypedProperties;
980
    }
981
982
    /**
983
     * Enable usage of php 7.4 type properties
984
     *
985
     * @param bool $useTypedProperties
986
     */
987 18
    public function useTypedProperties(bool $useTypedProperties = true): void
988
    {
989 18
        $this->useTypedProperties = $useTypedProperties;
990
    }
991
992
    /**
993
     * @return bool
994
     */
995
    public function getUseConstructorPropertyPromotion(): bool
996
    {
997
        return $this->useConstructorPropertyPromotion;
998
    }
999
1000
    /**
1001
     * Enable usage of PHP 8 promoted properties on constructor instead of array import
1002
     *
1003
     * @param bool $useConstructorPropertyPromotion
1004
     */
1005 5
    public function useConstructorPropertyPromotion(bool $useConstructorPropertyPromotion = true): void
1006
    {
1007 5
        $this->useConstructorPropertyPromotion = $useConstructorPropertyPromotion;
1008
    }
1009
}
1010
1011
/**
1012
 * @internal
1013
 */
1014
class PropertyGenerator
1015
{
1016
    private string $name;
1017
    private ?string $typeHint = null;
1018
    private bool $nullable = false;
1019
    private ?string $varTag = null;
1020
    private string $visibility = EntityGenerator::FIELD_VISIBLE_PROTECTED;
1021
    private $defaultValue;
1022
    private bool $hasDefaultValue = false;
1023
    private ?string $initialize = null;
1024
1025
    /**
1026
     * @param string $name
1027
     */
1028 35
    public function __construct(string $name)
1029
    {
1030 35
        $this->name = $name;
1031
    }
1032
1033 17
    public function setTypeHint(string $typeHint): void
1034
    {
1035 17
        $this->typeHint = $typeHint;
1036
    }
1037
1038 35
    public function setNullable(bool $nullable): void
1039
    {
1040 35
        $this->nullable = $nullable;
1041
    }
1042
1043 35
    public function setVarTag(string $varTag): void
1044
    {
1045 35
        $this->varTag = $varTag;
1046
    }
1047
1048 35
    public function setVisibility(string $visibility): void
1049
    {
1050 35
        $this->visibility = $visibility;
1051
    }
1052
1053 31
    public function setDefaultValue($value): void
1054
    {
1055 31
        $this->defaultValue = $value;
1056 31
        $this->hasDefaultValue = true;
1057
    }
1058
1059 30
    public function setInitialize(string $initialize): void
1060
    {
1061 30
        $this->initialize = $initialize;
1062
    }
1063
1064 27
    public function hasInitialisation(): bool
1065
    {
1066 27
        return $this->initialize !== null;
1067
    }
1068
1069 31
    public function addProperty(EntityClassGenerator $generator): void
1070
    {
1071 31
        $property = $generator->addProperty($this->name);
1072
1073 31
        $property
1074 31
            ->setNullable($this->nullable)
1075 31
            ->setVisibility($this->visibility)
1076 31
        ;
1077
1078 31
        if ($this->typeHint) {
1079 13
            $typehint = EntityGenerator::PROPERTY_TYPE_MAP[$this->typeHint] ?? $this->typeHint;
1080 13
            $property->setType($typehint);
1081
        }
1082
1083 31
        if ($this->hasDefaultValue) {
1084 27
            $property->setValue($this->defaultValue);
1085
        }
1086
1087 31
        if ($this->varTag) {
1088 31
            $type = $this->varTag;
1089
1090 31
            if (!isset(EntityGenerator::PROPERTY_TYPE_MAP[$this->varTag])) {
1091 31
                $type = $this->simplifyType($type, $generator);
1092
            }
1093
1094 31
            $property->addComment("\n@var $type");
1095
        }
1096
    }
1097
1098 4
    public function addPromotedProperty(Method $constructor, EntityClassGenerator $generator): void
1099
    {
1100 4
        $parameter = $constructor->addPromotedParameter($this->name);
1101
1102 4
        $parameter
1103 4
            ->setNullable($this->nullable)
1104 4
            ->setVisibility($this->visibility)
1105 4
        ;
1106
1107 4
        if ($this->typeHint) {
1108 4
            $typehint = EntityGenerator::PROPERTY_TYPE_MAP[$this->typeHint] ?? $this->typeHint;
1109 4
            $parameter->setType($typehint);
1110
        }
1111
1112 4
        if ($this->hasDefaultValue) {
1113 4
            $parameter->setDefaultValue($this->defaultValue);
1114
        }
1115
1116 4
        if ($this->varTag) {
1117 4
            $type = $this->varTag;
1118
1119 4
            if (!isset(EntityGenerator::PROPERTY_TYPE_MAP[$this->varTag])) {
1120 4
                $type = $this->simplifyType($type, $generator);
1121
            }
1122
1123 4
            $parameter->addComment("\n@var $type");
1124
        }
1125
    }
1126
1127 28
    public function addInitializeLine(Method $initializeMethod, string $assignationOperator = '='): void
1128
    {
1129 28
        if ($this->initialize) {
1130 28
            $initializeMethod->addBody('$this->'.$this->name.' '.$assignationOperator.' '.$this->initialize.';');
1131
        }
1132
    }
1133
1134 35
    private function simplifyType(string $type, EntityClassGenerator $generator): string
1135
    {
1136 35
        $types = explode('|', $type);
1137
1138 35
        foreach ($types as &$part) {
1139 35
            $atomicType = $part;
1140 35
            $isArray = false;
1141
1142 35
            if (str_ends_with($atomicType, '[]')) {
1143 19
                $atomicType = substr($atomicType, 0, -2);
1144 19
                $isArray = true;
1145
            }
1146
1147 35
            if (isset(EntityGenerator::PROPERTY_TYPE_MAP[$atomicType])) {
1148
                continue;
1149
            }
1150
1151 35
            $part = $generator->simplifyType($atomicType) . ($isArray ? '[]' : '');
1152
        }
1153
1154 35
        return implode('|', $types);
1155
    }
1156
}
1157
1158
/**
1159
 * @internal
1160
 */
1161
class ConfigurableEntityPrinter extends Printer
1162
{
1163 38
    public function __construct(EntityGenerator $generator)
1164
    {
1165 38
        parent::__construct();
1166
1167 38
        $this->linesBetweenMethods = 1;
1168 38
        $this->linesBetweenProperties = 1;
1169 38
        $this->indentation = str_repeat(' ', $generator->getNumSpaces());
1170
    }
1171
1172 38
    public function printClass($class, ?PhpNamespace $namespace = null): string
1173
    {
1174 38
        $code = parent::printClass($class, $namespace);
1175
1176
        // Reformat property docblock : nette will generate property doc on single line
1177 38
        return preg_replace('#^( *)/\*\*(.*)\s+\*/$#m', "$1/**\n$1 *$2\n$1 */", $code);
1178
    }
1179
}
1180
1181
/**
1182
 * @internal
1183
 */
1184
class EntityClassGenerator
1185
{
1186
    private ClassType $class;
1187
    private PhpNamespace $namespace;
1188
1189
    /**
1190
     * @param ClassType $class
1191
     * @param PhpNamespace $namespace
1192
     */
1193 38
    public function __construct(ClassType $class, PhpNamespace $namespace)
1194
    {
1195 38
        $this->class = $class;
1196 38
        $this->namespace = $namespace;
1197
    }
1198
1199
    /**
1200
     * Add use statement if necessary
1201
     * Ignore classes without namespace or in current namespace
1202
     *
1203
     * @param string $class
1204
     * @return void
1205
     */
1206 27
    public function addUse(string $class): void
1207
    {
1208 27
        $nsSeparatorPos = strrpos(ltrim($class, '\\'), '\\');
1209
1210
        // Not namespaced : do not import
1211 27
        if ($nsSeparatorPos === false) {
1212 2
            return;
1213
        }
1214
1215 27
        $ns = substr($class, 0, $nsSeparatorPos);
1216
1217
        // Same namespace : import is not necessary
1218 27
        if ($ns === $this->namespace->getName()) {
1219 27
            return;
1220
        }
1221
1222 4
        $this->namespace->addUse($class);
1223
    }
1224
1225
    /**
1226
     * Check if the given method exists on the current generated class
1227
     * This method will check parent class and used traits
1228
     *
1229
     * @param string $method
1230
     * @return bool
1231
     */
1232 38
    public function hasMethod(string $method): bool
1233
    {
1234
        /** @psalm-suppress InvalidArgument */
1235 38
        if ($this->class->getExtends() && method_exists($this->class->getExtends(), $method)) {
1236 2
            return true;
1237
        }
1238
1239 38
        foreach ($this->class->getTraits() as $trait) {
1240 2
            if ($trait instanceof TraitUse) {
1241
                $trait = $trait->getName();
1242
            }
1243
1244 2
            if (method_exists($trait, $method)) {
1245
                return true;
1246
            }
1247
        }
1248
1249 38
        return $this->class->hasMethod($method);
1250
    }
1251
1252
    /**
1253
     * Add a new method into the class
1254
     *
1255
     * @param string $name Method name
1256
     * @return Method
1257
     */
1258 36
    public function addMethod(string $name): Method
1259
    {
1260 36
        return $this->class->addMethod($name);
1261
    }
1262
1263
    /**
1264
     * Check if the given property exists on the current generated class
1265
     * This method will check parent class and used traits
1266
     * It will also check promoted parameters on constructor
1267
     *
1268
     * @param string $property
1269
     * @return bool
1270
     */
1271 38
    public function hasProperty(string $property): bool
1272
    {
1273 38
        if ($this->class->getExtends() && property_exists($this->class->getExtends(), $property)) {
1274 2
            return true;
1275
        }
1276
1277 38
        foreach ($this->class->getTraits() as $trait) {
1278 2
            if ($trait instanceof TraitUse) {
1279
                $trait = $trait->getName();
1280
            }
1281
1282 2
            if (property_exists($trait, $property)) {
1283
                return true;
1284
            }
1285
        }
1286
1287 38
        if ($this->class->hasProperty($property)) {
1288 4
            return true;
1289
        }
1290
1291 36
        if (!$this->class->hasMethod('__construct')) {
1292 34
            return false;
1293
        }
1294
1295 2
        $parameter = $this->class->getMethod('__construct')->getParameters()[$property] ?? null;
1296
1297 2
        return $parameter instanceof PromotedParameter;
1298
    }
1299
1300
    /**
1301
     * Get a method by its name
1302
     *
1303
     * @param string $name
1304
     * @return Method
1305
     */
1306 2
    public function getMethod(string $name): Method
1307
    {
1308 2
        return $this->class->getMethod($name);
1309
    }
1310
1311
    /**
1312
     * Add a new property on the entity class
1313
     *
1314
     * @param string $name Property name
1315
     *
1316
     * @return Property
1317
     */
1318 31
    public function addProperty(string $name): Property
1319
    {
1320 31
        return $this->class->addProperty($name);
1321
    }
1322
1323
    /**
1324
     * Simplify a typename if imported or in the current namespace
1325
     *
1326
     * @param string $type
1327
     * @return string
1328
     */
1329 35
    public function simplifyType(string $type): string
1330
    {
1331 35
        return $this->namespace->simplifyName($type);
1332
    }
1333
1334
    /**
1335
     * @param Method|Property|Constant|TraitUse $classMember
1336
     *
1337
     * @return void
1338
     */
1339 1
    public function addMember($classMember): void
1340
    {
1341 1
        $this->class->addMember($classMember);
1342
    }
1343
}
1344