Passed
Push — fix-FRAM-62-entity-generator-u... ( dd5324 )
by Vincent
06:37
created

EntityGenerator::getMethodTemplate()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 14
ccs 8
cts 9
cp 0.8889
rs 10
c 0
b 0
f 0
cc 4
nc 4
nop 1
crap 4.0218
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 34
    public function __construct(ServiceLocator $prime, ?InflectorObject $inflector = null)
324
    {
325 34
        $this->prime = $prime;
326 34
        $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 33
    public function generate($mapper, $file = null)
340
    {
341 33
        $this->isNew = !$file || !file_exists($file) || $this->regenerateEntityIfExists;
342
343
        // If entity doesn't exist or we're re-generating the entities entirely
344 33
        if ($this->isNew || !$file) {
345 30
            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 30
    public function generateEntityClass(Mapper $mapper)
362
    {
363 30
        $this->mapperInfo = $mapper->info();
364
365 30
        $this->staticReflection[$this->mapperInfo->className()] = ['properties' => [], 'methods' => []];
366
367
        $placeHolders = [
368
            '<namespace>',
369
            '<useStatement>',
370
            '<entityAnnotation>',
371
            '<entityClassName>',
372
            '<entityTraits>',
373
            '<entityBody>'
374
        ];
375
376
        $replacements = [
377 30
            $this->generateEntityNamespace(),
378 30
            $this->generateEntityUse(),
379 30
            $this->generateEntityDocBlock(),
380 30
            $this->generateEntityClassName(),
381 30
            $this->generateEntityTraits(),
382 30
            $this->generateEntityBody()
383
        ];
384
385 30
        $code = str_replace($placeHolders, $replacements, static::$classTemplate);
0 ignored issues
show
Bug introduced by
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 30
        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 30
    protected function generateEntityNamespace(): string
417
    {
418 30
        if ($this->hasNamespace($this->mapperInfo->className())) {
419 30
            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 30
    protected function generateEntityUse()
431
    {
432 30
        $use = [];
433
434 30
        if (($parentClass = $this->getClassToExtend()) && $this->hasNamespace($parentClass)) {
435 2
            $use[$parentClass] = 'use ' . $parentClass . ';';
436
        }
437
438 30
        foreach ($this->interfaces as $interface) {
439 3
            if ($this->hasNamespace($interface)) {
440 3
                $use[$interface] = 'use ' . $interface . ';';
441
            }
442
        }
443
444 30
        foreach ($this->traits as $trait) {
445 2
            if ($this->hasNamespace($trait)) {
446
                $use[$trait] = 'use ' . $trait . ';';
447
            }
448
        }
449
450 30
        foreach ($this->mapperInfo->objects() as $info) {
451 26
            $className = $info->className();
452 26
            if (!$info->belongsToRoot()) {
453 4
                continue;
454
            }
455
456 26
            if ($this->hasNamespace($className)) {
457 26
                $use[$className] = 'use '.$className.';';
458
            }
459
460 26
            if ($info->wrapper() !== null) {
461 1
                $repository = $this->prime->repository($className);
462 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

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 30
        if (!$use) {
471 5
            return '';
472
        }
473
474 26
        sort($use);
475
476 26
        return implode("\n", $use) . "\n\n";
477
    }
478
479
    /**
480
     * @return string
481
     */
482 30
    protected function generateEntityClassName()
483
    {
484 30
        return 'class ' . $this->getClassName($this->mapperInfo->className()) .
485 30
            ($this->classToExtend ? ' extends ' . $this->getClassToExtendName() : null) .
486 30
            ($this->interfaces ? ' implements ' . $this->getInterfacesToImplement() : null);
487
    }
488
489
    /**
490
     * @return string
491
     */
492 30
    protected function generateEntityTraits()
493
    {
494 30
        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 28
            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 33
    protected function generateEntityBody()
511
    {
512 33
        $fieldMappingProperties = $this->generateEntityFieldMappingProperties($this->useConstructorPropertyPromotion);
513 33
        $embeddedProperties = $this->generateEntityEmbeddedProperties($this->useConstructorPropertyPromotion);
514 33
        $stubMethods = $this->generateEntityStubMethods ? $this->generateEntityStubMethods() : null;
515
516 33
        $code = [];
517
518 33
        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 33
        $code[] = $this->generateEntityConstructor(
529 33
            $this->useConstructorPropertyPromotion,
530
            $fieldMappingProperties,
531
            $embeddedProperties
532
        );
533
534 33
        if ($stubMethods) {
535 31
            $code[] = $stubMethods;
536
        }
537
538 33
        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 33
    protected function generateEntityConstructor(bool $propertyPromotion, string $fieldMappingProperties, string $embeddedProperties)
549
    {
550 33
        $initializable = in_array(InitializableInterface::class, $this->interfaces);
551 33
        $isImportable  = in_array(ImportableInterface::class, $this->interfaces)
552 33
                    || is_subclass_of($this->classToExtend, ImportableInterface::class);
553
554 33
        $collections = [];
555
556
        // Assignment operator : use null coalesce assignment with property promotion
557
        // because assignation is performed before initializing default value
558 33
        $assign = $propertyPromotion ? '??=' : '=';
559
560 33
        foreach ($this->mapperInfo->objects() as $property) {
561 26
            if (!$property->belongsToRoot()) {
562 4
                continue;
563
            }
564
565 26
            if ($property->isRelation()) {
566 25
                if (!$property->isArray()) {
567 25
                    $collections[$property->name()] = '$this->'.$property->name().' '.$assign.' new '.$this->getRelativeClassName($property->className()).'();';
568 21
                } elseif ($property->wrapper() === 'collection') { // @todo handle other wrapper types
569 25
                    $collections[$property->name()] = '$this->'.$property->name().' '.$assign.' '.$this->getRelativeClassName($property->className()).'::collection();';
570
                }
571
            } else {
572 4
                $collections[$property->name()] = '$this->'.$property->name().' '.$assign.' new '.$this->getRelativeClassName($property->className()).'();';
573
            }
574
        }
575 33
        foreach ($this->mapperInfo->properties() as $property) {
576 33
            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 33
        $methods = [];
588
589 33
        if (!$this->hasMethod('__construct')) {
590 28
            if ($propertyPromotion) {
591 2
                $methods[] = $this->generateConstructorWithPromotedProperties($initializable, $collections, $fieldMappingProperties, $embeddedProperties);
592 26
            } elseif ($constructor = $this->generateClassicConstructor($isImportable, $initializable, $collections)) {
593 25
                $methods[] = $constructor;
594
            }
595
        }
596
597 33
        if (!$this->hasMethod('initialize') && $initializable) {
598 1
            $methods[] = $this->generateMethod('{@inheritdoc}', 'initialize', implode("\n".$this->spaces, $collections), 'void');
599
        }
600
601 33
        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 2
    private function generateConstructorWithPromotedProperties(bool $initializable, array $collections, string $fieldMappingProperties, string $embeddedProperties): string
615
    {
616 2
        if ($initializable) {
617
            $buffer = '$this->initialize();'."\n".$this->spaces;
618 2
        } 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 2
            $buffer = implode("\n".$this->spaces, $collections)."\n".$this->spaces;
620
        } else {
621
            $buffer = '';
622
        }
623
624 2
        $properties = rtrim($fieldMappingProperties."\n".$embeddedProperties);
625 2
        $properties = str_replace(';', ',', $properties);
626
627 2
        return $this->prefixCodeWithSpaces(str_replace(
628 2
            ['<body>', '<properties>'],
629 2
            [rtrim($buffer), $properties],
630 2
            static::$constructorWithPromotedPropertiesMethodTemplate
0 ignored issues
show
Bug introduced by
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
        ));
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
Bug introduced by
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
Bug introduced by
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 30
    protected function generateEntityDocBlock()
668
    {
669 30
        $lines = [];
670 30
        $lines[] = '/**';
671 30
        $lines[] = ' * ' . $this->getClassName($this->mapperInfo->className());
672 30
        $lines[] = ' */';
673
674 30
        return implode("\n", $lines);
675
    }
676
677
    /**
678
     * @return string
679
     */
680 32
    protected function generateEntityStubMethods()
681
    {
682 32
        $methods = [];
683
684 32
        foreach ($this->mapperInfo->properties() as $property) {
685 32
            if ($code = $this->generateEntityStubMethod('set', $property)) {
686 31
                $methods[] = $code;
687
            }
688
689 32
            if ($code = $this->generateEntityStubMethod('get', $property)) {
690 31
                $methods[] = $code;
691
            }
692
        }
693
694 32
        foreach ($this->mapperInfo->objects() as $property) {
695 25
            if (!$property->belongsToRoot()) {
696 4
                continue;
697
            }
698
699 25
            if (!$property->isArray() || $property->wrapper() !== null) {
700 25
                if ($code = $this->generateEntityStubMethod('set', $property)) {
701 25
                    $methods[] = $code;
702
                }
703 25
                if ($code = $this->generateEntityStubMethod('get', $property)) {
704 25
                    $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 32
        return implode("\n", $methods);
720
    }
721
722
    /**
723
     * @return string
724
     */
725 33
    protected function generateEntityFieldMappingProperties(bool $forceNullable = false)
726
    {
727 33
        $lines = [];
728
729 33
        foreach ($this->mapperInfo->properties() as  $property) {
730 33
            if ($this->hasProperty($property->name())) {
731 5
                continue;
732
            }
733
734 31
            $default = '';
735
736 31
            if ($property->hasDefault() && !$property->isDateTime()) {
737 5
                $default = ' = '.$this->stringfyValue(
738 5
                    $property->convert($property->getDefault())
739
                );
740 31
            } 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 31
            if (!$default && ($forceNullable || ($this->useTypedProperties && $property->isNullable()))) {
747 13
                $default = ' = null';
748
            }
749
750 31
            $lines[] = $this->generateFieldMappingPropertyDocBlock($property);
751 31
            $lines[] = $this->spaces.$this->fieldVisibility.$this->getPropertyTypeHintForSimpleProperty($property, $forceNullable).' $'.$property->name().$default.";\n";
752
        }
753
754 33
        return implode("\n", $lines);
755
    }
756
757
    /**
758
     * @param bool $forceNullable Force typehint to be nullable. Useful property promotion
759
     * @return string
760
     */
761 33
    protected function generateEntityEmbeddedProperties(bool $forceNullable = false)
762
    {
763 33
        $lines = [];
764
765 33
        foreach ($this->mapperInfo->objects() as $property) {
766 26
            if (!$property->belongsToRoot() || $this->hasProperty($property->name())) {
767 6
                continue;
768
            }
769
770 26
            if (!$property->isRelation()) {
771 4
                $lines[] = $this->generateEmbeddedPropertyDocBlock($property);
772 4
                $lines[] = $this->spaces . $this->fieldVisibility . $this->getPropertyTypeHintForObject($property, $forceNullable) . ' $'.$property->name().";\n";
773
            } else {
774 25
                $name = $property->name();
775 25
                $default = '';
776
777
                // Do not initialize the property if it's a wrapper
778 25
                if ($property->isArray() && $property->wrapper() === null) {
779 18
                    $default = ' = []';
780
                }
781
782
                // If property is typed, always define a default value
783 25
                if (($forceNullable || $this->useTypedProperties) && !$default) {
784 12
                    $default = ' = null';
785
                }
786
787 25
                $lines[] = $this->generateEmbeddedPropertyDocBlock($property);
788 25
                $lines[] = $this->spaces . $this->fieldVisibility . $this->getPropertyTypeHintForObject($property, $forceNullable) . ' $' . $name . $default .";\n";
789
            }
790
        }
791
792 33
        return implode("\n", $lines);
793
    }
794
795
    /**
796
     * @param string            $type
797
     * @param InfoInterface     $propertyInfo
798
     * @param string|null       $defaultValue
799
     *
800
     * @return string
801
     */
802 32
    protected function generateEntityStubMethod($type, InfoInterface $propertyInfo, $defaultValue = null)
803
    {
804 32
        $fieldName = $propertyInfo->name();
805
806
        // The hint flag help algorithm to determine the hint info for object parameter.
807
        // It should be 'array' for collection but the add method need the object hint.
808
        // setItems(array $items)
809
        // addItem(Item $item)
810 32
        $hintOne = false;
811
812 32
        if ($type === 'get' && $this->useGetShortcutMethod === true) {
813 31
            $variableName = $this->inflector->camelize($fieldName);
814 31
            $methodName = $variableName;
815
        } else {
816 32
            $methodName = $type . $this->inflector->classify($fieldName);
817 32
            $variableName = $this->inflector->camelize($fieldName);
818
        }
819
820 32
        if ($type === 'add') {
821 19
            $methodName = $this->inflector->singularize($methodName);
822 19
            $variableName = $this->inflector->singularize($variableName);
823 19
            $hintOne = true;
824
        }
825
826 32
        if ($this->hasMethod($methodName)) {
827 2
            return '';
828
        }
829 31
        $this->staticReflection[$this->mapperInfo->className()]['methods'][] = strtolower($methodName);
830
831 31
        if ($propertyInfo->isObject()) {
832
            /** @var ObjectPropertyInfo $propertyInfo */
833 25
            $variableType = $this->getRelativeClassName($propertyInfo->className());
834
            // Only makes nullable for single relation
835 25
            $methodTypeHint = $this->getPropertyTypeHint($propertyInfo->className(), !$hintOne && !$propertyInfo->isEmbedded());
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\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

835
            $methodTypeHint = $this->getPropertyTypeHint(/** @scrutinizer ignore-type */ $propertyInfo->className(), !$hintOne && !$propertyInfo->isEmbedded());
Loading history...
836
        } else {
837
            /** @var PropertyInfo $propertyInfo */
838 31
            $variableType = $propertyInfo->phpType();
839 31
            $methodTypeHint = $this->getPropertyTypeHint($variableType, $propertyInfo->isNullable());
840
        }
841
842 31
        if ($propertyInfo->isArray() && $hintOne === false) {
843 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

843
            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...
844
                /** @var ObjectPropertyInfo $propertyInfo */
845 1
                $repository = $this->prime->repository($propertyInfo->className());
846 1
                $wrapperClass = $this->getRelativeClassName($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

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

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

1323
                $type = $repository->collectionFactory()->wrapperClass(/** @scrutinizer ignore-type */ $property->wrapper());
Loading history...
1324
            }
1325
        }
1326
1327 12
        return ' ' . $this->getPropertyTypeHint($type, $forceNullable || $property->isRelation());
0 ignored issues
show
Bug introduced by
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

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