Issues (590)

src/Entity/EntityGenerator.php (20 issues)

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Bdf\Prime\Entity;
21
22
use Bdf\Prime\Mapper\Info\InfoInterface;
23
use Bdf\Prime\Mapper\Info\ObjectPropertyInfo;
24
use Bdf\Prime\Mapper\Info\PropertyInfo;
25
use Bdf\Prime\Mapper\Mapper;
26
use Bdf\Prime\Mapper\Info\MapperInfo;
27
use Bdf\Prime\ServiceLocator;
28
use Bdf\Prime\Types\PhpTypeInterface;
29
use Bdf\Prime\Types\TypeInterface;
30
use Doctrine\Common\Inflector\Inflector;
31
use Doctrine\Inflector\Inflector as InflectorObject;
32
use Doctrine\Inflector\InflectorFactory;
33
34
/**
35
 * Generic class used to generate PHP5 entity classes from Mapper.
36
 *
37
 *     [php]
38
 *     $mapper = $service->mappers()->build('Entity);
39
 *
40
 *     $generator = new EntityGenerator();
41
 *     $generator->setGenerateStubMethods(true);
42
 *     $generator->setRegenerateEntityIfExists(false);
43
 *     $generator->setUpdateEntityIfExists(true);
44
 *     $generator->generate($mapper, '/path/to/generate/entities');
45
 *
46
 *
47
 * @link    www.doctrine-project.org
48
 * @since   2.0
49
 * @author  Benjamin Eberlei <[email protected]>
50
 * @author  Guilherme Blanco <[email protected]>
51
 * @author  Jonathan Wage <[email protected]>
52
 * @author  Roman Borschel <[email protected]>
53
 */
54
class EntityGenerator
55
{
56
    // @todo should not be there : should be on PhpTypeInterface
57
    /**
58
     * Map prime types to php 7.4 property type
59
     */
60
    public const PROPERTY_TYPE_MAP = [
61
        PhpTypeInterface::BOOLEAN => 'bool',
62
        PhpTypeInterface::DOUBLE => 'float',
63
        PhpTypeInterface::INTEGER => 'int',
64
    ];
65
66
    /**
67
     * Specifies class fields should be protected.
68
     */
69
    public const FIELD_VISIBLE_PROTECTED = 'protected';
70
71
    /**
72
     * Specifies class fields should be private.
73
     */
74
    public const FIELD_VISIBLE_PRIVATE = 'private';
75
76
    /**
77
     * The prime service locator
78
     *
79
     * @var ServiceLocator
80
     */
81
    private $prime;
82
83
    /**
84
     * The inflector instance
85
     *
86
     * @var InflectorObject
87
     */
88
    private $inflector;
89
90
    /**
91
     * The mapper info
92
     *
93
     * @var MapperInfo
94
     */
95
    private $mapperInfo;
96
97
    /**
98
     * The extension to use for written php files.
99
     *
100
     * @var string
101
     */
102
    private $extension = '.php';
103
104
    /**
105
     * Whether or not the current Mapper instance is new or old.
106
     *
107
     * @var boolean
108
     */
109
    private $isNew = true;
110
111
    /**
112
     * @var array
113
     */
114
    private $staticReflection = [];
115
116
    /**
117
     * Number of spaces to use for indention in generated code.
118
     */
119
    private $numSpaces = 4;
120
121
    /**
122
     * The actual spaces to use for indention.
123
     *
124
     * @var string
125
     */
126
    private $spaces = '    ';
127
128
    /**
129
     * The class all generated entities should extend.
130
     *
131
     * @var string
132
     */
133
    private $classToExtend;
134
135
    /**
136
     * The interfaces all generated entities should implement.
137
     *
138
     * @var array
139
     */
140
    private $interfaces = [];
141
142
    /**
143
     * The traits
144
     *
145
     * @var array
146
     */
147
    private $traits = [];
148
149
    /**
150
     * Whether or not to generate sub methods.
151
     *
152
     * @var boolean
153
     */
154
    private $generateEntityStubMethods = true;
155
156
    /**
157
     * Whether or not to update the entity class if it exists already.
158
     *
159
     * @var boolean
160
     */
161
    private $updateEntityIfExists = false;
162
163
    /**
164
     * Whether or not to re-generate entity class if it exists already.
165
     *
166
     * @var boolean
167
     */
168
    private $regenerateEntityIfExists = false;
169
170
    /**
171
     * The name of get methods will not contains the 'get' prefix
172
     *
173
     * @var boolean
174
     */
175
    private $useGetShortcutMethod = true;
176
177
    /**
178
     * Visibility of the field
179
     *
180
     * @var string
181
     */
182
    private $fieldVisibility = self::FIELD_VISIBLE_PROTECTED;
183
184
    /**
185
     * Use type on generated properties
186
     * Note: only compatible with PHP >= 7.4
187
     *
188
     * @var bool
189
     */
190
    private $useTypedProperties = false;
191
192
    /**
193
     * Enable generation of PHP 8 constructor with promoted properties
194
     * If used, the constructor will not call import for filling the entity
195
     *
196
     * Note: only compatible with PHP >= 8.0
197
     *
198
     * @var bool
199
     */
200
    private $useConstructorPropertyPromotion = false;
201
202
    /**
203
     * @var string
204
     */
205
    private static $classTemplate =
206
'<?php
207
208
<namespace><useStatement><entityAnnotation>
209
<entityClassName>
210
{
211
<entityTraits><entityBody>
212
}
213
';
214
215
    /**
216
     * @var string
217
     */
218
    private static $getMethodTemplate =
219
'/**
220
 * <description>
221
 *
222
 * @return <variableType>
223
 */
224
public function <methodName>(): <methodTypeHint>
225
{
226
<spaces>return $this-><fieldName>;
227
}
228
';
229
230
    /**
231
     * @var string
232
     */
233
    private static $setMethodTemplate =
234
'/**
235
 * <description>
236
 *
237
 * @param <variableType> $<variableName>
238
 *
239
 * @return $this
240
 */
241
public function <methodName>(<methodTypeHint> $<variableName><variableDefault>): self
242
{
243
<spaces>$this-><fieldName> = $<variableName>;
244
245
<spaces>return $this;
246
}
247
';
248
249
    /**
250
     * @var string
251
     */
252
    private static $addMethodTemplate =
253
'/**
254
 * <description>
255
 *
256
 * @param <variableType> $<variableName>
257
 *
258
 * @return $this
259
 */
260
public function <methodName>(<methodTypeHint> $<variableName>): self
261
{
262
<spaces>$this-><fieldName>[] = $<variableName>;
263
264
<spaces>return $this;
265
}
266
';
267
268
    /**
269
     * @var string
270
     */
271
    private static $methodTemplate =
272
'/**
273
 * <description>
274
 */
275
public function <methodName>()<return>
276
{
277
<spaces><content>
278
}
279
';
280
281
    /**
282
     * @var string
283
     */
284
    private static $constructorMethodTemplate =
285
'/**
286
 * Constructor
287
 */
288
public function __construct()
289
{
290
<spaces><collections>
291
}
292
';
293
294
    /**
295
     * @var string
296
     */
297
    private static $importableConstructorMethodTemplate =
298
'/**
299
 * Constructor
300
 *
301
 * @param array $data
302
 */
303
public function __construct(array $data = [])
304
{
305
<spaces><initialize>$this->import($data);
306
}
307
';
308
309
    /**
310
     * @var string
311
     */
312
    private static $constructorWithPromotedPropertiesMethodTemplate =
313
'public function __construct(
314
<properties>
315
) {
316
<spaces><body>
317
}
318
';
319
320
    /**
321
     * Set prime service locator
322
     */
323 35
    public function __construct(ServiceLocator $prime, ?InflectorObject $inflector = null)
324
    {
325 35
        $this->prime = $prime;
326 35
        $this->inflector = $inflector ?? InflectorFactory::create()->build();
327
    }
328
329
    /**
330
     * Generates and writes entity classes
331
     *
332
     * @param Mapper $mapper
333
     * @param string $file    Entity file name
334
     *
335
     * @return string|false If no generation
336
     *
337
     * @api
338
     */
339 34
    public function generate($mapper, $file = null)
340
    {
341 34
        $this->isNew = !$file || !file_exists($file) || $this->regenerateEntityIfExists;
342
343
        // If entity doesn't exist or we're re-generating the entities entirely
344 34
        if ($this->isNew || !$file) {
345 31
            return $this->generateEntityClass($mapper);
346
        // If entity exists and we're allowed to update the entity class
347 3
        } elseif ($this->updateEntityIfExists && $file) {
348 3
            return $this->generateUpdatedEntityClass($mapper, $file);
349
        }
350
351
        return false;
352
    }
353
354
    /**
355
     * Generates a PHP5 Doctrine 2 entity class from the given Mapper instance.
356
     *
357
     * @param Mapper $mapper
358
     *
359
     * @return string
360
     */
361 31
    public function generateEntityClass(Mapper $mapper)
362
    {
363 31
        $this->mapperInfo = $mapper->info();
364
365 31
        $this->staticReflection[$this->mapperInfo->className()] = ['properties' => [], 'methods' => []];
366
367 31
        $placeHolders = [
368 31
            '<namespace>',
369 31
            '<useStatement>',
370 31
            '<entityAnnotation>',
371 31
            '<entityClassName>',
372 31
            '<entityTraits>',
373 31
            '<entityBody>'
374 31
        ];
375
376 31
        $replacements = [
377 31
            $this->generateEntityNamespace(),
378 31
            $this->generateEntityUse(),
379 31
            $this->generateEntityDocBlock(),
380 31
            $this->generateEntityClassName(),
381 31
            $this->generateEntityTraits(),
382 31
            $this->generateEntityBody()
383 31
        ];
384
385 31
        $code = str_replace($placeHolders, $replacements, static::$classTemplate);
0 ignored issues
show
Since $classTemplate is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $classTemplate to at least protected.
Loading history...
386
387 31
        return str_replace('<spaces>', $this->spaces, $code);
388
    }
389
390
    /**
391
     * Generates the updated code for the given Mapper and entity at path.
392
     *
393
     * @param Mapper $mapper
394
     * @param string $file
395
     *
396
     * @return string
397
     */
398 3
    public function generateUpdatedEntityClass(Mapper $mapper, $file)
399
    {
400 3
        $this->mapperInfo = $mapper->info();
401
402 3
        $currentCode = file_get_contents($file);
403
404 3
        $this->parseTokensInEntityFile($currentCode);
405
406 3
        $body = $this->generateEntityBody();
407 3
        $body = str_replace('<spaces>', $this->spaces, $body);
408 3
        $last = strrpos($currentCode, '}');
409
410 3
        return substr($currentCode, 0, $last) . $body . (strlen($body) > 0 ? "\n" : '') . "}\n";
411
    }
412
413
    /**
414
     * @return string
415
     */
416 31
    protected function generateEntityNamespace(): string
417
    {
418 31
        if ($this->hasNamespace($this->mapperInfo->className())) {
419 31
            return 'namespace ' . $this->getNamespace($this->mapperInfo->className()) .';' . "\n\n";
420
        }
421
422
        return '';
423
    }
424
425
    /**
426
     * Generate use part
427
     *
428
     * @return string
429
     */
430 31
    protected function generateEntityUse()
431
    {
432 31
        $use = [];
433
434 31
        if (($parentClass = $this->getClassToExtend()) && $this->hasNamespace($parentClass)) {
435 2
            $use[$parentClass] = 'use ' . $parentClass . ';';
436
        }
437
438 31
        foreach ($this->interfaces as $interface) {
439 3
            if ($this->hasNamespace($interface)) {
440 3
                $use[$interface] = 'use ' . $interface . ';';
441
            }
442
        }
443
444 31
        foreach ($this->traits as $trait) {
445 2
            if ($this->hasNamespace($trait)) {
446
                $use[$trait] = 'use ' . $trait . ';';
447
            }
448
        }
449
450 31
        foreach ($this->mapperInfo->objects() as $info) {
451 27
            $className = $info->className();
452 27
            if (!$info->belongsToRoot()) {
453 5
                continue;
454
            }
455
456 27
            if ($this->hasNamespace($className)) {
457 27
                $use[$className] = 'use '.$className.';';
458
            }
459
460 27
            if ($info->wrapper() !== null) {
461 1
                $repository = $this->prime->repository($className);
462 1
                $wrapperClass = $repository->collectionFactory()->wrapperClass($info->wrapper());
0 ignored issues
show
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

462
                $wrapperClass = $repository->collectionFactory()->wrapperClass(/** @scrutinizer ignore-type */ $info->wrapper());
Loading history...
463
464 1
                if ($this->hasNamespace($wrapperClass)) {
465 1
                    $use[$wrapperClass] = 'use '.$wrapperClass.';';
466
                }
467
            }
468
        }
469
470 31
        if (!$use) {
471 5
            return '';
472
        }
473
474 27
        sort($use);
475
476 27
        return implode("\n", $use) . "\n\n";
477
    }
478
479
    /**
480
     * @return string
481
     */
482 31
    protected function generateEntityClassName()
483
    {
484 31
        return 'class ' . $this->getClassName($this->mapperInfo->className()) .
485 31
            ($this->classToExtend ? ' extends ' . $this->getClassToExtendName() : null) .
486 31
            ($this->interfaces ? ' implements ' . $this->getInterfacesToImplement() : null);
487
    }
488
489
    /**
490
     * @return string
491
     */
492 31
    protected function generateEntityTraits()
493
    {
494 31
        if (!$this->traits) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->traits of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
495 29
            return '';
496
        }
497
498 2
        $traits = '';
499
500 2
        foreach ($this->traits as $trait) {
501 2
            $traits .= $this->spaces . 'use ' . $this->getRelativeClassName($trait) . ';' . "\n";
502
        }
503
504 2
        return $traits . "\n";
505
    }
506
507
    /**
508
     * @return string
509
     */
510 34
    protected function generateEntityBody()
511
    {
512 34
        $fieldMappingProperties = $this->generateEntityFieldMappingProperties($this->useConstructorPropertyPromotion);
513 34
        $embeddedProperties = $this->generateEntityEmbeddedProperties($this->useConstructorPropertyPromotion);
514 34
        $stubMethods = $this->generateEntityStubMethods ? $this->generateEntityStubMethods() : null;
515
516 34
        $code = [];
517
518 34
        if (!$this->useConstructorPropertyPromotion) {
519 31
            if ($fieldMappingProperties) {
520 29
                $code[] = $fieldMappingProperties;
521
            }
522
523 31
            if ($embeddedProperties) {
524 25
                $code[] = $embeddedProperties;
525
            }
526
        }
527
528 34
        $code[] = $this->generateEntityConstructor(
529 34
            $this->useConstructorPropertyPromotion,
530 34
            $fieldMappingProperties,
531 34
            $embeddedProperties
532 34
        );
533
534 34
        if ($stubMethods) {
535 32
            $code[] = $stubMethods;
536
        }
537
538 34
        return implode("\n", $code);
539
    }
540
541
    /**
542
     * @param bool $propertyPromotion Generate constructor with property promotion
543
     * @param string $fieldMappingProperties Entity properties
544
     * @param string $embeddedProperties Relations and embedded properties
545
     *
546
     * @return string
547
     */
548 34
    protected function generateEntityConstructor(bool $propertyPromotion, string $fieldMappingProperties, string $embeddedProperties)
549
    {
550 34
        $initializable = in_array(InitializableInterface::class, $this->interfaces);
551 34
        $isImportable  = in_array(ImportableInterface::class, $this->interfaces)
552 34
                    || is_subclass_of($this->classToExtend, ImportableInterface::class);
553
554 34
        $collections = [];
555
556
        // Assignment operator : use null coalesce assignment with property promotion
557
        // because assignation is performed before initializing default value
558 34
        $assign = $propertyPromotion ? '??=' : '=';
559
560 34
        foreach ($this->mapperInfo->objects() as $property) {
561 27
            if (!$property->belongsToRoot()) {
562 5
                continue;
563
            }
564
565 27
            if ($property->isRelation()) {
566 26
                if (!$property->isArray()) {
567 26
                    $collections[$property->name()] = '$this->'.$property->name().' '.$assign.' new '.$this->getRelativeClassName($property->className()).'();';
568 21
                } elseif ($property->wrapper() === 'collection') { // @todo handle other wrapper types
569 26
                    $collections[$property->name()] = '$this->'.$property->name().' '.$assign.' '.$this->getRelativeClassName($property->className()).'::collection();';
570
                }
571
            } else {
572 5
                $collections[$property->name()] = '$this->'.$property->name().' '.$assign.' new '.$this->getRelativeClassName($property->className()).'();';
573
            }
574
        }
575 34
        foreach ($this->mapperInfo->properties() as $property) {
576 34
            if ($property->isDateTime() && $property->hasDefault()) {
577 4
                $constructorArgs = '';
578
                // Add the default timezone from the property type.
579 4
                if ($timezone = $property->getTimezone()) {
580 4
                    $constructorArgs = "'now', new \DateTimeZone('$timezone')";
581
                }
582
583 4
                $collections[$property->name()] = '$this->'.$property->name().' '.$assign.' new '.$property->phpType().'('.$constructorArgs.');';
584
            }
585
        }
586
587 34
        $methods = [];
588
589 34
        if (!$this->hasMethod('__construct')) {
590 29
            if ($propertyPromotion) {
591 3
                $methods[] = $this->generateConstructorWithPromotedProperties($initializable, $collections, $fieldMappingProperties, $embeddedProperties);
592 26
            } elseif ($constructor = $this->generateClassicConstructor($isImportable, $initializable, $collections)) {
593 25
                $methods[] = $constructor;
594
            }
595
        }
596
597 34
        if (!$this->hasMethod('initialize') && $initializable) {
598 1
            $methods[] = $this->generateMethod('{@inheritdoc}', 'initialize', implode("\n".$this->spaces, $collections), 'void');
599
        }
600
601 34
        return implode("\n", $methods);
602
    }
603
604
    /**
605
     * Generate PHP 8 constructor
606
     *
607
     * @param bool $initializable Does the entity class implements InitializableInterface ?
608
     * @param string[] $collections Initialisation method instructions
609
     * @param string $fieldMappingProperties Entity properties
610
     * @param string $embeddedProperties Relations and embedded properties
611
     *
612
     * @return string
613
     */
614 3
    private function generateConstructorWithPromotedProperties(bool $initializable, array $collections, string $fieldMappingProperties, string $embeddedProperties): string
615
    {
616 3
        if ($initializable) {
617
            $buffer = '$this->initialize();'."\n".$this->spaces;
618 3
        } elseif ($collections) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $collections of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
619 3
            $buffer = implode("\n".$this->spaces, $collections)."\n".$this->spaces;
620
        } else {
621
            $buffer = '';
622
        }
623
624 3
        $properties = rtrim($fieldMappingProperties."\n".$embeddedProperties);
625 3
        $properties = str_replace(';', ',', $properties);
626
627 3
        return $this->prefixCodeWithSpaces(str_replace(
628 3
            ['<body>', '<properties>'],
629 3
            [rtrim($buffer), $properties],
630 3
            static::$constructorWithPromotedPropertiesMethodTemplate
0 ignored issues
show
Since $constructorWithPromotedPropertiesMethodTemplate is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $constructorWithPromotedPropertiesMethodTemplate to at least protected.
Loading history...
631 3
        ));
632
    }
633
634
    /**
635
     * Generate classic constructor
636
     *
637
     * @param bool $isImportable Does the entity class implements InitializableInterface ?
638
     * @param bool $initializable Does the entity class implements ImportableInterface ?
639
     * @param string[] $collections Initialisation method instructions
640
     *
641
     * @return string|null
642
     */
643 26
    private function generateClassicConstructor(bool $isImportable, bool $initializable, array $collections): ?string
644
    {
645 26
        if ($isImportable) {
646 2
            $buffer = '';
647
648 2
            if ($initializable) {
649 1
                $buffer = '$this->initialize();'."\n".$this->spaces;
650 1
            } elseif ($collections) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $collections of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
651 1
                $buffer = implode("\n".$this->spaces, $collections)."\n".$this->spaces;
652
            }
653
654 2
            return $this->prefixCodeWithSpaces(str_replace("<initialize>", $buffer, static::$importableConstructorMethodTemplate));
0 ignored issues
show
Since $importableConstructorMethodTemplate is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $importableConstructorMethodTemplate to at least protected.
Loading history...
655 24
        } elseif ($collections && !$initializable) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $collections of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
656 23
            return $this->prefixCodeWithSpaces(str_replace("<collections>", implode("\n".$this->spaces, $collections), static::$constructorMethodTemplate));
0 ignored issues
show
Since $constructorMethodTemplate is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $constructorMethodTemplate to at least protected.
Loading history...
657
        }
658
659 2
        return null;
660
    }
661
662
    /**
663
     * @param Mapper $mapper
664
     *
665
     * @return string
666
     */
667 31
    protected function generateEntityDocBlock()
668
    {
669 31
        $lines = [];
670 31
        $lines[] = '/**';
671 31
        $lines[] = ' * ' . $this->getClassName($this->mapperInfo->className());
672 31
        $lines[] = ' */';
673
674 31
        return implode("\n", $lines);
675
    }
676
677
    /**
678
     * @return string
679
     */
680 33
    protected function generateEntityStubMethods()
681
    {
682 33
        $methods = [];
683
684 33
        foreach ($this->mapperInfo->properties() as $property) {
685 33
            if ($code = $this->generateEntityStubMethod('set', $property)) {
686 32
                $methods[] = $code;
687
            }
688
689 33
            if ($code = $this->generateEntityStubMethod('get', $property)) {
690 32
                $methods[] = $code;
691
            }
692
        }
693
694 33
        foreach ($this->mapperInfo->objects() as $property) {
695 26
            if (!$property->belongsToRoot()) {
696 5
                continue;
697
            }
698
699 26
            if (!$property->isArray() || $property->wrapper() !== null) {
700 26
                if ($code = $this->generateEntityStubMethod('set', $property)) {
701 26
                    $methods[] = $code;
702
                }
703 26
                if ($code = $this->generateEntityStubMethod('get', $property)) {
704 26
                    $methods[] = $code;
705
                }
706
            } else {
707 19
                if ($code = $this->generateEntityStubMethod('add', $property)) {
708 19
                    $methods[] = $code;
709
                }
710 19
                if ($code = $this->generateEntityStubMethod('set', $property)) {
711 19
                    $methods[] = $code;
712
                }
713 19
                if ($code = $this->generateEntityStubMethod('get', $property)) {
714 19
                    $methods[] = $code;
715
                }
716
            }
717
        }
718
719 33
        return implode("\n", $methods);
720
    }
721
722
    /**
723
     * @return string
724
     */
725 34
    protected function generateEntityFieldMappingProperties(bool $forceNullable = false)
726
    {
727 34
        $lines = [];
728
729 34
        foreach ($this->mapperInfo->properties() as  $property) {
730 34
            if ($this->hasProperty($property->name())) {
731 5
                continue;
732
            }
733
734 32
            $default = '';
735
736 32
            if ($property->hasDefault() && !$property->isDateTime()) {
737 5
                $default = ' = '.$this->stringfyValue(
738 5
                    $property->convert($property->getDefault())
739 5
                );
740 32
            } elseif ($property->isArray()) {
741 17
                $default = ' = []';
742
            }
743
744
            // A nullable property should be defined as null by default
745
            // A property is considered as nullable if it's explicitly defined on mapper or if the field is auto-generated
746 32
            if (!$default && ($forceNullable || ($this->useTypedProperties && $property->isNullable()))) {
747 14
                $default = ' = null';
748
            }
749
750 32
            $lines[] = $this->generateFieldMappingPropertyDocBlock($property);
751 32
            $lines[] = $this->spaces.$this->fieldVisibility.$this->getPropertyTypeHintForSimpleProperty($property, $forceNullable).' $'.$property->name().$default.";\n";
752
        }
753
754 34
        return implode("\n", $lines);
755
    }
756
757
    /**
758
     * @param bool $forceNullable Force typehint to be nullable. Useful property promotion
759
     * @return string
760
     */
761 34
    protected function generateEntityEmbeddedProperties(bool $forceNullable = false)
762
    {
763 34
        $lines = [];
764
765 34
        foreach ($this->mapperInfo->objects() as $property) {
766 27
            if (!$property->belongsToRoot() || $this->hasProperty($property->name())) {
767 7
                continue;
768
            }
769
770 27
            if (!$property->isRelation()) {
771
                // Always add a default value with use property promotion
772 5
                $default = $this->useConstructorPropertyPromotion ? ' = null' : '';
773
774 5
                $lines[] = $this->generateEmbeddedPropertyDocBlock($property);
775 5
                $lines[] = $this->spaces . $this->fieldVisibility . $this->getPropertyTypeHintForObject($property, $forceNullable) . ' $'.$property->name().$default.";\n";
776
            } else {
777 26
                $name = $property->name();
778 26
                $default = '';
779
780
                // Do not initialize the property if it's a wrapper
781 26
                if ($property->isArray() && $property->wrapper() === null) {
782 18
                    $default = ' = []';
783
                }
784
785
                // If property is typed, always define a default value
786 26
                if (($forceNullable || $this->useTypedProperties) && !$default) {
787 13
                    $default = ' = null';
788
                }
789
790 26
                $lines[] = $this->generateEmbeddedPropertyDocBlock($property);
791 26
                $lines[] = $this->spaces . $this->fieldVisibility . $this->getPropertyTypeHintForObject($property, $forceNullable) . ' $' . $name . $default .";\n";
792
            }
793
        }
794
795 34
        return implode("\n", $lines);
796
    }
797
798
    /**
799
     * @param string            $type
800
     * @param InfoInterface     $propertyInfo
801
     * @param string|null       $defaultValue
802
     *
803
     * @return string
804
     */
805 33
    protected function generateEntityStubMethod($type, InfoInterface $propertyInfo, $defaultValue = null)
806
    {
807 33
        $fieldName = $propertyInfo->name();
808
809
        // The hint flag help algorithm to determine the hint info for object parameter.
810
        // It should be 'array' for collection but the add method need the object hint.
811
        // setItems(array $items)
812
        // addItem(Item $item)
813 33
        $hintOne = false;
814
815 33
        if ($type === 'get' && $this->useGetShortcutMethod === true) {
816 32
            $variableName = $this->inflector->camelize($fieldName);
817 32
            $methodName = $variableName;
818
        } else {
819 33
            $methodName = $type . $this->inflector->classify($fieldName);
820 33
            $variableName = $this->inflector->camelize($fieldName);
821
        }
822
823 33
        if ($type === 'add') {
824 19
            $methodName = $this->inflector->singularize($methodName);
825 19
            $variableName = $this->inflector->singularize($variableName);
826 19
            $hintOne = true;
827
        }
828
829 33
        if ($this->hasMethod($methodName)) {
830 2
            return '';
831
        }
832 32
        $this->staticReflection[$this->mapperInfo->className()]['methods'][] = strtolower($methodName);
833
834 32
        if ($propertyInfo->isObject()) {
835
            /** @var ObjectPropertyInfo $propertyInfo */
836 26
            $variableType = $this->getRelativeClassName($propertyInfo->className());
837
            // Only makes nullable for single relation
838 26
            $methodTypeHint = $this->getPropertyTypeHint($propertyInfo->className(), !$hintOne && !$propertyInfo->isEmbedded());
0 ignored issues
show
It seems like $propertyInfo->className() can also be of type null; however, parameter $type of Bdf\Prime\Entity\EntityG...::getPropertyTypeHint() 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

838
            $methodTypeHint = $this->getPropertyTypeHint(/** @scrutinizer ignore-type */ $propertyInfo->className(), !$hintOne && !$propertyInfo->isEmbedded());
Loading history...
839
        } else {
840
            /** @var PropertyInfo $propertyInfo */
841 32
            $variableType = $propertyInfo->phpType();
842 32
            $methodTypeHint = $this->getPropertyTypeHint($variableType, $propertyInfo->isNullable());
843
        }
844
845 32
        if ($propertyInfo->isArray() && $hintOne === false) {
846 20
            if ($propertyInfo->isObject() && $propertyInfo->wrapper() !== null) {
0 ignored issues
show
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

846
            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...
847
                /** @var ObjectPropertyInfo $propertyInfo */
848 1
                $repository = $this->prime->repository($propertyInfo->className());
849 1
                $wrapperClass = $this->getRelativeClassName($repository->collectionFactory()->wrapperClass($propertyInfo->wrapper()));
0 ignored issues
show
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

849
                $wrapperClass = $this->getRelativeClassName($repository->collectionFactory()->wrapperClass(/** @scrutinizer ignore-type */ $propertyInfo->wrapper()));
Loading history...
850
851 1
                $methodTypeHint = $wrapperClass;
852 1
                $variableType .= '[]|'.$wrapperClass;
853
            } else {
854 19
                $methodTypeHint = 'array';
855
856 19
                if ($variableType !== 'array') {
857 19
                    $variableType .= '[]';
858
                }
859
            }
860
        }
861
862 32
        $replacements = [
863 32
          '<description>'       => ucfirst($type).' '.$variableName,
864 32
          '<methodTypeHint>'    => $methodTypeHint,
865 32
          '<variableType>'      => $variableType,
866 32
          '<variableName>'      => $variableName,
867 32
          '<methodName>'        => $methodName,
868 32
          '<fieldName>'         => $fieldName,
869 32
          '<variableDefault>'   => ($defaultValue !== null) ? (' = '.$defaultValue) : ''
870 32
        ];
871
872 32
        $method = str_replace(
873 32
            array_keys($replacements),
874 32
            array_values($replacements),
875 32
            $this->getMethodTemplate($type)
876 32
        );
877
878 32
        return $this->prefixCodeWithSpaces($method);
879
    }
880
881
    /**
882
     * Get the template of the method
883
     *
884
     * @param string $prefix
885
     *
886
     * @return string
887
     *
888
     * @throws \LogicException
889
     */
890 32
    private function getMethodTemplate($prefix)
891
    {
892
        switch ($prefix) {
893 32
            case 'get':
894 32
                return static::$getMethodTemplate;
0 ignored issues
show
Since $getMethodTemplate is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $getMethodTemplate to at least protected.
Loading history...
895
896 32
            case 'add':
897 19
                return static::$addMethodTemplate;
0 ignored issues
show
Since $addMethodTemplate is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $addMethodTemplate to at least protected.
Loading history...
898
899 32
            case 'set':
900 32
                return static::$setMethodTemplate;
0 ignored issues
show
Since $setMethodTemplate is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $setMethodTemplate to at least protected.
Loading history...
901
        }
902
903
        throw new \LogicException('No template found for method "'.$prefix.'"');
904
    }
905
906
    /**
907
     * @param string $description
908
     * @param string $methodName
909
     * @param string $content
910
     *
911
     * @return string
912
     */
913 1
    protected function generateMethod(string $description, string $methodName, string $content, ?string $return = null)
914
    {
915 1
        if ($this->hasMethod($methodName)) {
916
            return '';
917
        }
918
919 1
        $this->staticReflection[$this->mapperInfo->className()]['methods'][] = $methodName;
920
921 1
        $replacements = [
922 1
            '<description>' => $description,
923 1
            '<methodName>'  => $methodName,
924 1
            '<content>'     => $content,
925 1
            '<return>'      => $return ? ": $return" : '',
926 1
        ];
927
928 1
        $method = str_replace(
929 1
            array_keys($replacements),
930 1
            array_values($replacements),
931 1
            static::$methodTemplate
0 ignored issues
show
Since $methodTemplate is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $methodTemplate to at least protected.
Loading history...
932 1
        );
933
934 1
        return $this->prefixCodeWithSpaces($method);
935
    }
936
937
    /**
938
     * @param PropertyInfo $property
939
     *
940
     * @return string
941
     */
942 32
    protected function generateFieldMappingPropertyDocBlock($property)
943
    {
944 32
        $lines = [];
945 32
        $lines[] = $this->spaces . '/**';
946 32
        $lines[] = $this->spaces . ' * @var '.$property->phpType();
947 32
        $lines[] = $this->spaces . ' */';
948
949 32
        return implode("\n", $lines);
950
    }
951
952
    /**
953
     * @param ObjectPropertyInfo $property
954
     *
955
     * @return string
956
     */
957 27
    protected function generateEmbeddedPropertyDocBlock($property)
958
    {
959 27
        $className = $property->className();
960 27
        if ($className) {
961 27
            $className = $this->getRelativeClassName($className);
962
963 27
            if ($property->isArray()) {
964 19
                if ($property->wrapper() !== null) {
965 1
                    $repository = $this->prime->repository($property->className());
966 1
                    $className = $this->getRelativeClassName($repository->collectionFactory()->wrapperClass($property->wrapper())).'|'.$className.'[]';
0 ignored issues
show
It seems like $property->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

966
                    $className = $this->getRelativeClassName($repository->collectionFactory()->wrapperClass(/** @scrutinizer ignore-type */ $property->wrapper())).'|'.$className.'[]';
Loading history...
967
                } else {
968 27
                    $className .= '[]';
969
                }
970
            }
971
        } else {
972
            $className = '{type}';
973
        }
974
975 27
        $lines = [];
976 27
        $lines[] = $this->spaces . '/**';
977 27
        $lines[] = $this->spaces . ' * @var '.$className;
978 27
        $lines[] = $this->spaces . ' */';
979
980 27
        return implode("\n", $lines);
981
    }
982
983
    //
984
    //---------- tools methods
985
    //
986
987
    /**
988
     * @todo this won't work if there is a namespace in brackets and a class outside of it.
989
     *
990
     * @param string $src
991
     *
992
     * @return void
993
     */
994 3
    protected function parseTokensInEntityFile($src)
995
    {
996 3
        $tokens = token_get_all($src);
997 3
        $lastSeenNamespace = "";
998 3
        $lastSeenClass = false;
999
1000 3
        $inNamespace = false;
1001 3
        $inClass = false;
1002
1003 3
        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...
1004 3
            $token = $tokens[$i];
1005 3
            if (in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT])) {
1006 3
                continue;
1007
            }
1008
1009 3
            if ($inNamespace) {
1010 3
                if ($token[0] == T_NS_SEPARATOR || $token[0] == T_STRING || (defined('T_NAME_QUALIFIED') && $token[0] == T_NAME_QUALIFIED)) {
1011 3
                    $lastSeenNamespace .= $token[1];
1012 3
                } elseif (is_string($token) && in_array($token, [';', '{'])) {
1013 3
                    $inNamespace = false;
1014
                }
1015
            }
1016
1017 3
            if ($inClass) {
1018 3
                $inClass = false;
1019 3
                $lastSeenClass = $lastSeenNamespace . ($lastSeenNamespace ? '\\' : '') . $token[1];
1020 3
                $this->staticReflection[$lastSeenClass]['properties'] = [];
1021 3
                $this->staticReflection[$lastSeenClass]['methods'] = [];
1022
            }
1023
1024 3
            if ($token[0] == T_NAMESPACE) {
1025 3
                $lastSeenNamespace = "";
1026 3
                $inNamespace = true;
1027 3
            } elseif ($token[0] == T_CLASS && $tokens[$i-1][0] != T_DOUBLE_COLON) {
1028 3
                $inClass = true;
1029 3
            } elseif ($token[0] == T_FUNCTION) {
1030 3
                if ($tokens[$i+2][0] == T_STRING) {
1031 3
                    $this->staticReflection[$lastSeenClass]['methods'][] = strtolower($tokens[$i+2][1]);
1032
                } elseif ($tokens[$i+2] == "&" && $tokens[$i+3][0] == T_STRING) {
1033 3
                    $this->staticReflection[$lastSeenClass]['methods'][] = strtolower($tokens[$i+3][1]);
1034
                }
1035 3
            } elseif (in_array($token[0], [T_VAR, T_PUBLIC, T_PRIVATE, T_PROTECTED]) && $tokens[$i+2][0] != T_FUNCTION) {
1036 3
                $this->staticReflection[$lastSeenClass]['properties'][] = substr($tokens[$i+2][1], 1);
1037
            }
1038
        }
1039
    }
1040
1041
    /**
1042
     * @param string $property
1043
     *
1044
     * @return bool
1045
     */
1046 34
    protected function hasProperty($property)
1047
    {
1048 34
        if ($this->classToExtend || (!$this->isNew && class_exists($this->mapperInfo->className()))) {
1049
            // don't generate property if its already on the base class.
1050 5
            $reflClass = new \ReflectionClass($this->getClassToExtend() ?: $this->mapperInfo->className());
1051 5
            if ($reflClass->hasProperty($property)) {
1052 2
                return true;
1053
            }
1054
        }
1055
1056
        // check traits for existing property
1057 34
        foreach ($this->getTraitsReflections() as $trait) {
1058
            if ($trait->hasProperty($property)) {
1059
                return true;
1060
            }
1061
        }
1062
1063 34
        return (
1064 34
            isset($this->staticReflection[$this->mapperInfo->className()]) &&
1065 34
            in_array($property, $this->staticReflection[$this->mapperInfo->className()]['properties'])
1066 34
        );
1067
    }
1068
1069
    /**
1070
     * @param string $method
1071
     *
1072
     * @return bool
1073
     */
1074 34
    protected function hasMethod($method)
1075
    {
1076 34
        if ($this->classToExtend || (!$this->isNew && class_exists($this->mapperInfo->className()))) {
1077
            // don't generate method if its already on the base class.
1078 5
            $reflClass = new \ReflectionClass($this->getClassToExtend() ?: $this->mapperInfo->className());
1079
1080 5
            if ($reflClass->hasMethod($method)) {
1081 2
                return true;
1082
            }
1083
        }
1084
1085
        // check traits for existing method
1086 34
        foreach ($this->getTraitsReflections() as $trait) {
1087
            if ($trait->hasMethod($method)) {
1088
                return true;
1089
            }
1090
        }
1091
1092 34
        return (
1093 34
            isset($this->staticReflection[$this->mapperInfo->className()]) &&
1094 34
            in_array(strtolower($method), $this->staticReflection[$this->mapperInfo->className()]['methods'])
1095 34
        );
1096
    }
1097
1098
    /**
1099
     * Get class name relative to use
1100
     *
1101
     * @param string $className
1102
     * @return string
1103
     */
1104 30
    protected function getRelativeClassName($className)
1105
    {
1106 30
        $className = ltrim($className, '\\');
1107
1108 30
        if ($this->hasNamespace($className)) {
1109 27
            return $this->getClassName($className);
1110
        } else {
1111 7
            return '\\' . $className;
1112
        }
1113
    }
1114
1115
    /**
1116
     * Get the class short name
1117
     *
1118
     * @param string $className
1119
     *
1120
     * @return string
1121
     */
1122 31
    protected function getClassName($className)
1123
    {
1124 31
        $parts = explode('\\', $className);
1125 31
        return array_pop($parts);
1126
    }
1127
1128
    /**
1129
     * @param string $className
1130
     *
1131
     * @return string
1132
     */
1133 31
    protected function getNamespace($className)
1134
    {
1135 31
        $parts = explode('\\', $className);
1136 31
        array_pop($parts);
1137
1138 31
        return implode('\\', $parts);
1139
    }
1140
1141
    /**
1142
     * @param string $className
1143
     *
1144
     * @return bool
1145
     */
1146 31
    protected function hasNamespace($className)
1147
    {
1148 31
        return strrpos($className, '\\') != 0;
1149
    }
1150
1151
    /**
1152
     * @return string
1153
     */
1154 2
    protected function getClassToExtendName()
1155
    {
1156 2
        $refl = new \ReflectionClass($this->getClassToExtend());
1157
1158 2
        return $this->getRelativeClassName($refl->getName());
1159
    }
1160
1161
    /**
1162
     * @return string
1163
     */
1164 3
    protected function getInterfacesToImplement()
1165
    {
1166 3
        $interfaces = [];
1167
1168 3
        foreach ($this->interfaces as $interface) {
1169 3
            $refl = new \ReflectionClass($interface);
1170
1171 3
            $interfaces[] = $this->getRelativeClassName($refl->getName());
1172
        }
1173
1174 3
        return implode(', ', $interfaces);
1175
    }
1176
1177
    /**
1178
     * @param Mapper $mapper
1179
     *
1180
     * @return \ReflectionClass[]
1181
     */
1182 34
    protected function getTraitsReflections()
1183
    {
1184 34
        if ($this->isNew) {
1185 31
            return [];
1186
        }
1187
1188 3
        $reflClass = new \ReflectionClass($this->mapperInfo->className());
1189
1190 3
        $traits = [];
1191
1192 3
        while ($reflClass !== false) {
1193 3
            $traits = array_merge($traits, $reflClass->getTraits());
1194
1195 3
            $reflClass = $reflClass->getParentClass();
1196
        }
1197
1198 3
        return $traits;
1199
    }
1200
1201
    /**
1202
     * @param string $code
1203
     * @param int    $num
1204
     *
1205
     * @return string
1206
     */
1207 33
    protected function prefixCodeWithSpaces($code, $num = 1)
1208
    {
1209 33
        $lines = explode("\n", $code);
1210
1211 33
        foreach ($lines as $key => $value) {
1212 33
            if (! empty($value)) {
1213 33
                $lines[$key] = str_repeat($this->spaces, $num) . $lines[$key];
1214
            }
1215
        }
1216
1217 33
        return implode("\n", $lines);
1218
    }
1219
1220
    /**
1221
     * Get string representation of a value
1222
     *
1223
     * @param mixed $value
1224
     *
1225
     * @return string
1226
     */
1227 5
    protected function stringfyValue($value)
1228
    {
1229 5
        if (is_array($value)) {
1230
            if (empty($value)) {
1231
                return '[]';
1232
            }
1233
1234
            return var_export($value, true);
1235
        }
1236
1237 5
        if (null === $value) {
1238
            return 'null';
1239
        }
1240
1241 5
        if (is_string($value)) {
1242 4
            return "'" . $value . "'";
1243
        }
1244
1245 2
        if (is_bool($value)) {
1246 2
            return $value ? 'true' : 'false';
1247
        }
1248
1249
        return $value;
1250
    }
1251
1252
    /**
1253
     * Get the php 7.4 property type hint
1254
     *
1255
     * This method will map invalid type hint to value one (ex: double -> float)
1256
     * It will also resolve the relative class name if the type is a class
1257
     *
1258
     * @param string $type The property type declared by field phpType()
1259
     * @param bool $nullable Does the property should be nullable
1260
     *
1261
     * @return string
1262
     *
1263
     * @see TypeInterface::phpType()
1264
     *
1265
     * @todo use directly the property info object ?
1266
     */
1267 32
    protected function getPropertyTypeHint(string $type, bool $nullable): string
1268
    {
1269 32
        if (class_exists($type)) {
1270 29
            $type = $this->getRelativeClassName($type);
1271
        } else {
1272 32
            $type = self::PROPERTY_TYPE_MAP[$type] ?? $type;
1273
        }
1274
1275 32
        return ($nullable ? '?' : '') . $type;
1276
    }
1277
1278
    /**
1279
     * Get the php 7.4 property type hint for a simple property
1280
     *
1281
     * If typed properties are disabled, this method will return an empty string
1282
     * The returned type hint will be prefixed by a single space
1283
     *
1284
     * @param PropertyInfo $property
1285
     * @param bool $forceNullable Force typehint to be nullable. Useful property promotion
1286
     *
1287
     * @return string
1288
     */
1289 32
    protected function getPropertyTypeHintForSimpleProperty(PropertyInfo $property, bool $forceNullable = false): string
1290
    {
1291 32
        if (!$this->useTypedProperties) {
1292 27
            return '';
1293
        }
1294
1295 16
        return ' ' . $this->getPropertyTypeHint($property->phpType(), $forceNullable || $property->isNullable());
1296
    }
1297
1298
    /**
1299
     * Get the php 7.4 property type hint for a object property (i.e. relation or embedded)
1300
     *
1301
     * If typed properties are disabled, this method will return an empty string
1302
     * The returned type hint will be prefixed by a single space
1303
     *
1304
     * - Embedded properties will not be marked as nullable
1305
     * - Relations will always be nullable
1306
     * - This method will also resolve collection relations and the wrapper class if provided
1307
     *
1308
     * @param ObjectPropertyInfo $property
1309
     * @param bool $forceNullable Force typehint to be nullable. Useful property promotion
1310
     *
1311
     * @return string
1312
     */
1313 27
    protected function getPropertyTypeHintForObject(ObjectPropertyInfo $property, bool $forceNullable = false): string
1314
    {
1315 27
        if (!$this->useTypedProperties) {
1316 23
            return '';
1317
        }
1318
1319 13
        $type = $property->className();
1320
1321 13
        if ($property->isArray()) {
1322 8
            if ($property->wrapper() === null) {
1323 7
                $type = 'array';
1324
            } else {
1325 1
                $repository = $this->prime->repository($type);
1326 1
                $type = $repository->collectionFactory()->wrapperClass($property->wrapper());
0 ignored issues
show
It seems like $property->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

1326
                $type = $repository->collectionFactory()->wrapperClass(/** @scrutinizer ignore-type */ $property->wrapper());
Loading history...
1327
            }
1328
        }
1329
1330 13
        return ' ' . $this->getPropertyTypeHint($type, $forceNullable || $property->isRelation());
0 ignored issues
show
It seems like $type can also be of type null; however, parameter $type of Bdf\Prime\Entity\EntityG...::getPropertyTypeHint() 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

1330
        return ' ' . $this->getPropertyTypeHint(/** @scrutinizer ignore-type */ $type, $forceNullable || $property->isRelation());
Loading history...
1331
    }
1332
1333
    //---------------------- mutators
1334
1335
    /**
1336
     * Sets the number of spaces the exported class should have.
1337
     *
1338
     * @api
1339
     */
1340 1
    public function setNumSpaces(int $numSpaces): void
1341
    {
1342 1
        $this->spaces = str_repeat(' ', $numSpaces);
1343 1
        $this->numSpaces = $numSpaces;
1344
    }
1345
1346
    /**
1347
     * Gets the indentation spaces
1348
     */
1349 1
    public function getNumSpaces(): int
1350
    {
1351 1
        return $this->numSpaces;
1352
    }
1353
1354
    /**
1355
     * Sets the extension to use when writing php files to disk.
1356
     *
1357
     * @api
1358
     */
1359 1
    public function setExtension(string $extension): void
1360
    {
1361 1
        $this->extension = $extension;
1362
    }
1363
1364
    /**
1365
     * Get the file extension
1366
     */
1367 1
    public function getExtension(): string
1368
    {
1369 1
        return $this->extension;
1370
    }
1371
1372
    /**
1373
     * Sets the name of the class the generated classes should extend from.
1374
     *
1375
     * @api
1376
     */
1377 6
    public function setClassToExtend(string $classToExtend): void
1378
    {
1379 6
        $this->classToExtend = $classToExtend;
1380
    }
1381
1382
    /**
1383
     * Get the class to extend
1384
     */
1385 35
    public function getClassToExtend(): ?string
1386
    {
1387 35
        return $this->classToExtend;
1388
    }
1389
1390
    /**
1391
     * Add interface to implement
1392
     *
1393
     * @api
1394
     *
1395
     * @return void
1396
     */
1397 2
    public function addInterface(string $interface): void
1398
    {
1399 2
        $this->interfaces[$interface] = $interface;
1400
    }
1401
1402
    /**
1403
     * Sets the interfaces
1404
     *
1405
     * @param string[] $interfaces
1406
     *
1407
     * @api
1408
     */
1409 2
    public function setInterfaces(array $interfaces): void
1410
    {
1411 2
        $this->interfaces = $interfaces;
1412
    }
1413
1414
    /**
1415
     * Get the registered interfaces
1416
     */
1417 1
    public function getInterfaces(): array
1418
    {
1419 1
        return $this->interfaces;
1420
    }
1421
1422
    /**
1423
     * Add trait
1424
     *
1425
     * @api
1426
     *
1427
     * @return void
1428
     */
1429 2
    public function addTrait(string $trait): void
1430
    {
1431 2
        $this->traits[$trait] = $trait;
1432
    }
1433
1434
    /**
1435
     * Sets the traits
1436
     *
1437
     * @param string[] $traits
1438
     *
1439
     * @api
1440
     */
1441 1
    public function setTraits(array $traits): void
1442
    {
1443 1
        $this->traits = $traits;
1444
    }
1445
1446
    /**
1447
     * Get the registered traits
1448
     */
1449 1
    public function getTraits(): array
1450
    {
1451 1
        return $this->traits;
1452
    }
1453
1454
    /**
1455
     * Sets the class fields visibility for the entity (can either be private or protected).
1456
     *
1457
     * @throws \InvalidArgumentException
1458
     *
1459
     * @api
1460
     */
1461 2
    public function setFieldVisibility(string $visibility): void
1462
    {
1463 2
        if ($visibility !== static::FIELD_VISIBLE_PRIVATE && $visibility !== static::FIELD_VISIBLE_PROTECTED) {
1464
            throw new \InvalidArgumentException('Invalid provided visibility (only private and protected are allowed): ' . $visibility);
1465
        }
1466
1467 2
        $this->fieldVisibility = $visibility;
1468
    }
1469
1470
    /**
1471
     * Get the field visibility
1472
     */
1473 1
    public function getFieldVisibility(): string
1474
    {
1475 1
        return $this->fieldVisibility;
1476
    }
1477
1478
    /**
1479
     * Sets whether or not to try and update the entity if it already exists.
1480
     *
1481
     * @api
1482
     */
1483 4
    public function setUpdateEntityIfExists(bool $bool): void
1484
    {
1485 4
        $this->updateEntityIfExists = $bool;
1486
    }
1487
1488
    /**
1489
     * Get the flag for updating the entity
1490
     */
1491 1
    public function getUpdateEntityIfExists(): bool
1492
    {
1493 1
        return $this->updateEntityIfExists;
1494
    }
1495
1496
    /**
1497
     * Sets whether or not to regenerate the entity if it exists.
1498
     *
1499
     * @api
1500
     */
1501 1
    public function setRegenerateEntityIfExists(bool $bool): void
1502
    {
1503 1
        $this->regenerateEntityIfExists = $bool;
1504
    }
1505
1506
    /**
1507
     * Get the flag for regenerating entity
1508
     */
1509 1
    public function getRegenerateEntityIfExists(): bool
1510
    {
1511 1
        return $this->regenerateEntityIfExists;
1512
    }
1513
1514
    /**
1515
     * Sets whether or not to generate stub methods for the entity.
1516
     *
1517
     * @api
1518
     */
1519 2
    public function setGenerateStubMethods(bool $bool): void
1520
    {
1521 2
        $this->generateEntityStubMethods = $bool;
1522
    }
1523
1524
    /**
1525
     * Get the flag for generating stub methods
1526
     */
1527 1
    public function getGenerateStubMethods(): bool
1528
    {
1529 1
        return $this->generateEntityStubMethods;
1530
    }
1531
1532
    /**
1533
     * Sets whether or not the get mehtod will be suffixed by 'get'.
1534
     *
1535
     * @param bool $bool
1536
     *
1537
     * @return void
1538
     *
1539
     * @api
1540
     */
1541 2
    public function useGetShortcutMethod($bool = true)
1542
    {
1543 2
        $this->useGetShortcutMethod = $bool;
1544
    }
1545
1546
    /**
1547
     * Get the flag for get mehtod name.
1548
     */
1549 1
    public function getUseGetShortcutMethod(): bool
1550
    {
1551 1
        return $this->useGetShortcutMethod;
1552
    }
1553
1554
    /**
1555
     * @return bool
1556
     */
1557
    public function getUseTypedProperties(): bool
1558
    {
1559
        return $this->useTypedProperties;
1560
    }
1561
1562
    /**
1563
     * Enable usage of php 7.4 type properties
1564
     *
1565
     * @param bool $useTypedProperties
1566
     */
1567 16
    public function useTypedProperties(bool $useTypedProperties = true): void
1568
    {
1569 16
        $this->useTypedProperties = $useTypedProperties;
1570
    }
1571
1572
    /**
1573
     * @return bool
1574
     */
1575
    public function getUseConstructorPropertyPromotion(): bool
1576
    {
1577
        return $this->useConstructorPropertyPromotion;
1578
    }
1579
1580
    /**
1581
     * Enable usage of PHP 8 promoted properties on constructor instead of array import
1582
     *
1583
     * @param bool $useConstructorPropertyPromotion
1584
     */
1585 3
    public function useConstructorPropertyPromotion(bool $useConstructorPropertyPromotion = true): void
1586
    {
1587 3
        $this->useConstructorPropertyPromotion = $useConstructorPropertyPromotion;
1588
    }
1589
}
1590