• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nextras / orm / 13354993605

16 Feb 2025 12:29PM UTC coverage: 91.73% (-0.3%) from 92.021%
13354993605

push

github

web-flow
Merge pull request #732 from nextras/datetime-property-wrapper

Datetime property wrapper

94 of 112 new or added lines in 15 files covered. (83.93%)

1 existing line in 1 file now uncovered.

4115 of 4486 relevant lines covered (91.73%)

4.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.04
/src/Entity/Reflection/MetadataParser.php
1
<?php declare(strict_types = 1);
2

3
/** @noinspection PhpUnused */
4

5
namespace Nextras\Orm\Entity\Reflection;
6

7

8
use BackedEnum;
9
use DateTime;
10
use Nette\Utils\Reflection;
11
use Nextras\Orm\Collection\ICollection;
12
use Nextras\Orm\Entity\Embeddable\EmbeddableContainer;
13
use Nextras\Orm\Entity\Embeddable\IEmbeddable;
14
use Nextras\Orm\Entity\IEntity;
15
use Nextras\Orm\Entity\IProperty;
16
use Nextras\Orm\Entity\PropertyWrapper\BackedEnumWrapper;
17
use Nextras\Orm\Entity\PropertyWrapper\DateTimeWrapper;
18
use Nextras\Orm\Entity\PropertyWrapper\PrimaryProxyWrapper;
19
use Nextras\Orm\Exception\InvalidStateException;
20
use Nextras\Orm\Exception\NotSupportedException;
21
use Nextras\Orm\Relationships\HasMany;
22
use Nextras\Orm\Relationships\ManyHasMany;
23
use Nextras\Orm\Relationships\ManyHasOne;
24
use Nextras\Orm\Relationships\OneHasMany;
25
use Nextras\Orm\Relationships\OneHasOne;
26
use Nextras\Orm\Repository\IRepository;
27
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
28
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
29
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
30
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
31
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
32
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
33
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
34
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
35
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
36
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
37
use PHPStan\PhpDocParser\Lexer\Lexer;
38
use PHPStan\PhpDocParser\Parser\ConstExprParser;
39
use PHPStan\PhpDocParser\Parser\PhpDocParser;
40
use PHPStan\PhpDocParser\Parser\TokenIterator;
41
use PHPStan\PhpDocParser\Parser\TypeParser;
42
use PHPStan\PhpDocParser\ParserConfig;
43
use ReflectionClass;
44
use function array_keys;
45
use function assert;
46
use function class_exists;
47
use function count;
48
use function is_subclass_of;
49
use function strlen;
50
use function substr;
51
use function trigger_error;
52

53

54
class MetadataParser implements IMetadataParser
55
{
56
        /** @var array<string, callable|string> */
57
        protected array $modifiers = [
58
                '1:1' => 'parseOneHasOneModifier',
59
                '1:m' => 'parseOneHasManyModifier',
60
                'm:1' => 'parseManyHasOneModifier',
61
                'm:m' => 'parseManyHasManyModifier',
62
                'enum' => 'parseEnumModifier',
63
                'virtual' => 'parseVirtualModifier',
64
                'container' => 'parseContainerModifier',
65
                'wrapper' => 'parseWrapperModifier',
66
                'default' => 'parseDefaultModifier',
67
                'primary' => 'parsePrimaryModifier',
68
                'primary-proxy' => 'parsePrimaryProxyModifier',
69
                'embeddable' => 'parseEmbeddableModifier',
70
        ];
71

72
        /** @var ReflectionClass<object> */
73
        protected $reflection;
74

75
        /** @var ReflectionClass<object> */
76
        protected $currentReflection;
77

78
        /** @var EntityMetadata */
79
        protected $metadata;
80

81
        /** @var array<class-string<IEntity>, class-string<IRepository<IEntity>>> */
82
        protected $entityClassesMap;
83

84
        /** @var ModifierParser */
85
        protected $modifierParser;
86

87
        /** @var array<string, PropertyMetadata[]> */
88
        protected $classPropertiesCache = [];
89

90
        protected PhpDocParser $phpDocParser;
91
        protected Lexer $phpDocLexer;
92

93

94
        /**
95
         * @param array<string, string> $entityClassesMap
96
         * @param array<class-string<IEntity>, class-string<IRepository<IEntity>>> $entityClassesMap
97
         */
98
        public function __construct(array $entityClassesMap)
99
        {
100
                $this->entityClassesMap = $entityClassesMap;
5✔
101
                $this->modifierParser = new ModifierParser();
5✔
102

103
                // phpdoc-parser 2.0
104
                if (class_exists('PHPStan\PhpDocParser\ParserConfig')) {
5✔
105
                        $config = new ParserConfig(usedAttributes: []); // @phpstan-ignore-line
4✔
106
                        $this->phpDocLexer = new Lexer($config); // @phpstan-ignore-line
4✔
107
                        $constExprParser = new ConstExprParser($config); // @phpstan-ignore-line
4✔
108
                        $typeParser = new TypeParser($config, $constExprParser); // @phpstan-ignore-line
4✔
109
                        $this->phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); // @phpstan-ignore-line
4✔
110
                } else {
111
                        $this->phpDocLexer = new Lexer(); // @phpstan-ignore-line
1✔
112
                        $constExprParser = new ConstExprParser(); // @phpstan-ignore-line
1✔
113
                        $typeParser = new TypeParser($constExprParser); // @phpstan-ignore-line
1✔
114
                        $this->phpDocParser = new PhpDocParser($typeParser, $constExprParser); // @phpstan-ignore-line
1✔
115
                }
116
        }
5✔
117

118

119
        /**
120
         * Adds modifier processor.
121
         * @return static
122
         */
123
        public function addModifier(string $modifier, callable $processor)
124
        {
125
                $this->modifiers[strtolower($modifier)] = $processor;
×
126
                return $this;
×
127
        }
128

129

130
        public function parseMetadata(string $entityClass, array|null &$fileDependencies): EntityMetadata
131
        {
132
                $this->reflection = new ReflectionClass($entityClass);
5✔
133
                $this->metadata = new EntityMetadata($entityClass);
5✔
134

135
                $this->loadProperties($fileDependencies);
5✔
136
                $this->initPrimaryKey();
5✔
137

138
                if ($fileDependencies !== null) {
5✔
139
                        $fileDependencies = array_values(array_unique($fileDependencies));
5✔
140
                }
141
                return $this->metadata;
5✔
142
        }
143

144

145
        /**
146
         * @param list<string>|null $fileDependencies
147
         */
148
        protected function loadProperties(array|null &$fileDependencies): void
149
        {
150
                $classTree = [$current = $this->reflection->name];
5✔
151
                while (($current = get_parent_class($current)) !== false) {
5✔
152
                        $classTree[] = $current;
5✔
153
                }
154

155
                $methods = [];
5✔
156
                foreach ($this->reflection->getMethods() as $method) {
5✔
157
                        $methods[strtolower($method->name)] = true;
5✔
158
                }
159

160
                foreach (array_reverse($classTree) as $class) {
5✔
161
                        if (!isset($this->classPropertiesCache[$class])) {
5✔
162
                                $traits = class_uses($class);
5✔
163
                                foreach ($traits !== false ? $traits : [] as $traitName) {
5✔
164
                                        assert(trait_exists($traitName));
165
                                        $reflectionTrait = new ReflectionClass($traitName);
5✔
166
                                        $file = $reflectionTrait->getFileName();
5✔
167
                                        if ($file !== false) $fileDependencies[] = $file;
5✔
168
                                        $this->currentReflection = $reflectionTrait;
5✔
169
                                        $this->classPropertiesCache[$traitName] = $this->parseAnnotations($reflectionTrait, $methods);
5✔
170
                                }
171

172
                                $reflection = new ReflectionClass($class);
5✔
173
                                $file = $reflection->getFileName();
5✔
174
                                if ($file !== false) $fileDependencies[] = $file;
5✔
175
                                $this->currentReflection = $reflection;
5✔
176
                                $this->classPropertiesCache[$class] = $this->parseAnnotations($reflection, $methods);
5✔
177
                        }
178

179
                        $traits = class_uses($class);
5✔
180
                        foreach ($traits !== false ? $traits : [] as $traitName) {
5✔
181
                                foreach ($this->classPropertiesCache[$traitName] as $name => $property) {
5✔
182
                                        $this->metadata->setProperty($name, $property);
5✔
183
                                }
184
                        }
185

186
                        foreach ($this->classPropertiesCache[$class] as $name => $property) {
5✔
187
                                $this->metadata->setProperty($name, $property);
5✔
188
                        }
189
                }
190
        }
5✔
191

192

193
        /**
194
         * @param ReflectionClass<object> $reflection
195
         * @param array<string, true> $methods
196
         * @return array<string, PropertyMetadata>
197
         */
198
        protected function parseAnnotations(ReflectionClass $reflection, array $methods): array
199
        {
200
                $docComment = $reflection->getDocComment();
5✔
201
                if ($docComment === false) return [];
5✔
202

203
                $tokens = new TokenIterator($this->phpDocLexer->tokenize($docComment));
5✔
204
                $phpDocNode = $this->phpDocParser->parse($tokens);
5✔
205

206
                $properties = [];
5✔
207
                foreach ($phpDocNode->getPropertyTagValues() as $propertyTagValue) {
5✔
208
                        $property = $this->parseProperty($propertyTagValue, $reflection->getName(), $methods, isReadonly: false);
5✔
209
                        $properties[$property->name] = $property;
5✔
210
                }
211
                foreach ($phpDocNode->getPropertyWriteTagValues() as $propertyTagValue) {
5✔
212
                        $property = $this->parseProperty($propertyTagValue, $reflection->getName(), $methods, isReadonly: false);
×
213
                        $properties[$property->name] = $property;
×
214
                }
215
                foreach ($phpDocNode->getPropertyReadTagValues() as $propertyTagValue) {
5✔
216
                        $property = $this->parseProperty($propertyTagValue, $reflection->getName(), $methods, isReadonly: true);
5✔
217
                        $properties[$property->name] = $property;
5✔
218
                }
219
                return $properties;
5✔
220
        }
221

222

223
        /**
224
         * @param array<string, true> $methods
225
         */
226
        protected function parseProperty(
227
                PropertyTagValueNode $propertyNode,
228
                string $containerClassName,
229
                array $methods,
230
                bool $isReadonly,
231
        ): PropertyMetadata
232
        {
233
                $property = new PropertyMetadata();
5✔
234
                $property->name = substr($propertyNode->propertyName, 1);
5✔
235
                $property->containerClassname = $containerClassName;
5✔
236
                $property->isReadonly = $isReadonly;
5✔
237

238
                $this->parseAnnotationTypes($property, $propertyNode->type);
5✔
239
                $this->parseAnnotationValue($property, $propertyNode->description);
5✔
240
                $this->processPropertyGettersSetters($property, $methods);
5✔
241
                $this->processDefaultPropertyWrappers($property);
5✔
242
                return $property;
5✔
243
        }
244

245

246
        protected function parseAnnotationTypes(PropertyMetadata $property, TypeNode $type): void
247
        {
248
                static $aliases = [
5✔
249
                        'double' => 'float',
250
                        'real' => 'float',
251
                        'numeric' => 'float',
252
                        'number' => 'float',
253
                        'integer' => 'int',
254
                ];
255

256
                if ($type instanceof UnionTypeNode) {
5✔
257
                        $types = $type->types;
5✔
258
                } elseif ($type instanceof IntersectionTypeNode) {
5✔
259
                        $types = $type->types;
×
260
                } else {
261
                        $types = [$type];
5✔
262
                }
263

264
                $parsedTypes = [];
5✔
265
                foreach ($types as $subType) {
5✔
266
                        if ($subType instanceof NullableTypeNode) {
5✔
267
                                $property->isNullable = true;
5✔
268
                                $subType = $subType->type;
5✔
269
                        }
270
                        if ($subType instanceof GenericTypeNode) {
5✔
271
                                $subType = $subType->type;
5✔
272
                        }
273

274
                        if ($subType instanceof IdentifierTypeNode) {
5✔
275
                                $subTypeName = $subType->name;
5✔
276
                                if ($subTypeName === 'boolean') $subTypeName = 'bool'; // avoid expansion, bug in Nette
5✔
277
                                $expandedSubType = Reflection::expandClassName($subTypeName, $this->currentReflection);
5✔
278
                                $expandedSubTypeLower = strtolower($expandedSubType);
5✔
279

280
                                if ($expandedSubTypeLower === 'null') {
5✔
281
                                        $property->isNullable = true;
5✔
282
                                        continue;
5✔
283
                                }
284
                                if ($expandedSubType === DateTime::class || is_subclass_of($expandedSubType, DateTime::class)) {
5✔
285
                                        throw new NotSupportedException("Type '{$expandedSubType}' in {$this->currentReflection->name}::\${$property->name} property is not supported anymore. Use \DateTimeImmutable or \Nextras\Dbal\Utils\DateTimeImmutable type.");
×
286
                                }
287
                                if (isset($aliases[$expandedSubTypeLower])) {
5✔
288
                                        /** @var string $expandedSubType */
289
                                        $expandedSubType = $aliases[$expandedSubTypeLower];
×
290
                                }
291
                                $parsedTypes[$expandedSubType] = true;
5✔
292
                        } elseif ($subType instanceof ArrayTypeNode) {
5✔
293
                                $parsedTypes['array'] = true;
5✔
294
                        } elseif ($subType instanceof ArrayShapeNode) {
5✔
295
                                $parsedTypes['array'] = true;
5✔
296
                        } elseif ($subType instanceof ObjectShapeNode) {
×
297
                                $parsedTypes['object'] = true;
×
298
                        } else {
299
                                throw new NotSupportedException("Type '{$type}' in {$this->currentReflection->name}::\${$property->name} property is not supported. For Nextras Orm purpose simplify it.");
×
300
                        }
301
                }
302

303
                if (count($parsedTypes) < 1) {
5✔
304
                        throw new NotSupportedException("Property {$this->currentReflection->name}::\${$property->name} without a type definition is not supported.");
×
305
                }
306
                $property->types = $parsedTypes;
5✔
307
        }
5✔
308

309

310
        protected function parseAnnotationValue(PropertyMetadata $property, string $propertyComment): void
311
        {
312
                if (strlen($propertyComment) === 0) {
5✔
313
                        return;
5✔
314
                }
315

316
                $matches = $this->modifierParser->matchModifiers($propertyComment);
5✔
317
                foreach ($matches as $macroContent) {
5✔
318
                        try {
319
                                $args = $this->modifierParser->parse($macroContent, $this->currentReflection);
5✔
320
                        } catch (InvalidModifierDefinitionException $e) {
5✔
321
                                throw new InvalidModifierDefinitionException(
5✔
322
                                        "Invalid modifier definition for {$this->currentReflection->name}::\${$property->name} property.",
5✔
323
                                        0,
5✔
324
                                        $e,
325
                                );
326
                        }
327
                        $this->processPropertyModifier($property, $args[0], $args[1]);
5✔
328
                }
329
        }
5✔
330

331

332
        /**
333
         * @param array<string, true> $methods
334
         */
335
        protected function processPropertyGettersSetters(PropertyMetadata $property, array $methods): void
336
        {
337
                $getter = 'getter' . strtolower($property->name);
5✔
338
                if (isset($methods[$getter])) {
5✔
339
                        $property->hasGetter = $getter;
5✔
340
                }
341
                $setter = 'setter' . strtolower($property->name);
5✔
342
                if (isset($methods[$setter])) {
5✔
343
                        $property->hasSetter = $setter;
×
344
                }
345
        }
5✔
346

347

348
        protected function processDefaultPropertyWrappers(PropertyMetadata $property): void
349
        {
350
                if ($property->wrapper !== null) return;
5✔
351

352
                foreach ($property->types as $type => $_) {
5✔
353
                        if (is_subclass_of($type, \DateTimeImmutable::class) || $type === \DateTimeImmutable::class) {
5✔
354
                                $property->wrapper = DateTimeWrapper::class;
5✔
355
                        } elseif (is_subclass_of($type, BackedEnum::class)) {
5✔
356
                                $property->wrapper = BackedEnumWrapper::class;
5✔
357
                        }
358
                }
359
        }
5✔
360

361

362
        /**
363
         * @param array<int|string, mixed> $args
364
         */
365
        protected function processPropertyModifier(PropertyMetadata $property, string $modifier, array $args): void
366
        {
367
                $type = strtolower($modifier);
5✔
368
                if (!isset($this->modifiers[$type])) {
5✔
369
                        throw new InvalidModifierDefinitionException(
5✔
370
                                "Unknown modifier '$type' type for {$this->currentReflection->name}::\${$property->name} property.",
5✔
371
                        );
372
                }
373

374
                $callback = $this->modifiers[$type];
5✔
375
                if (!is_array($callback)) {
5✔
376
                        $callback = [$this, $callback];
5✔
377
                }
378
                assert(is_callable($callback));
379
                call_user_func_array($callback, [$property, &$args]);
5✔
380
                if (count($args) > 0) {
5✔
381
                        $parts = [];
5✔
382
                        foreach ($args as $key => $val) {
5✔
383
                                if (is_numeric($key) && !is_array($val)) {
5✔
384
                                        $parts[] = $val;
5✔
385
                                        continue;
5✔
386
                                }
387
                                $parts[] = $key;
5✔
388
                        }
389
                        throw new InvalidModifierDefinitionException(
5✔
390
                                "Modifier {{$type}} in {$this->currentReflection->name}::\${$property->name} property has unknown arguments: " . implode(', ', $parts) . '.',
5✔
391
                        );
392
                }
393
        }
5✔
394

395

396
        /**
397
         * @param array<int|string, mixed> $args
398
         */
399
        protected function parseOneHasOneModifier(PropertyMetadata $property, array &$args): void
400
        {
401
                $property->relationship = new PropertyRelationshipMetadata();
5✔
402
                $property->relationship->type = PropertyRelationshipMetadata::ONE_HAS_ONE;
5✔
403
                $property->wrapper = OneHasOne::class;
5✔
404
                $this->processRelationshipIsMain($property, $args);
5✔
405
                $this->processRelationshipEntityProperty($property, $args);
5✔
406
                $this->processRelationshipCascade($property, $args);
5✔
407
                assert($property->relationship !== null);
408
                $property->isVirtual = !$property->relationship->isMain;
5✔
409
        }
5✔
410

411

412
        /**
413
         * @param array<int|string, mixed> $args
414
         */
415
        protected function parseOneHasManyModifier(PropertyMetadata $property, array &$args): void
416
        {
417
                $property->relationship = new PropertyRelationshipMetadata();
5✔
418
                $property->relationship->type = PropertyRelationshipMetadata::ONE_HAS_MANY;
5✔
419
                $property->wrapper = OneHasMany::class;
5✔
420
                $property->isVirtual = true;
5✔
421
                $this->processRelationshipEntityProperty($property, $args);
5✔
422
                $this->processRelationshipCascade($property, $args);
5✔
423
                $this->processRelationshipOrder($property, $args);
5✔
424
                $this->processRelationshipExposeCollection($property, $args);
5✔
425
        }
5✔
426

427

428
        /**
429
         * @param array<int|string, mixed> $args
430
         */
431
        protected function parseManyHasOneModifier(PropertyMetadata $property, array &$args): void
432
        {
433
                $property->relationship = new PropertyRelationshipMetadata();
5✔
434
                $property->relationship->type = PropertyRelationshipMetadata::MANY_HAS_ONE;
5✔
435
                $property->wrapper = ManyHasOne::class;
5✔
436
                $this->processRelationshipEntityProperty($property, $args);
5✔
437
                $this->processRelationshipCascade($property, $args);
5✔
438
        }
5✔
439

440

441
        /**
442
         * @param array<int|string, mixed> $args
443
         */
444
        protected function parseManyHasManyModifier(PropertyMetadata $property, array &$args): void
445
        {
446
                $property->relationship = new PropertyRelationshipMetadata();
5✔
447
                $property->relationship->type = PropertyRelationshipMetadata::MANY_HAS_MANY;
5✔
448
                $property->wrapper = ManyHasMany::class;
5✔
449
                $property->isVirtual = true;
5✔
450
                $this->processRelationshipIsMain($property, $args);
5✔
451
                $this->processRelationshipEntityProperty($property, $args);
5✔
452
                $this->processRelationshipCascade($property, $args);
5✔
453
                $this->processRelationshipOrder($property, $args);
5✔
454
                $this->processRelationshipExposeCollection($property, $args);
5✔
455
        }
5✔
456

457

458
        /**
459
         * @param array<int|string, mixed> $args
460
         */
461
        protected function parseEnumModifier(PropertyMetadata $property, array &$args): void
462
        {
463
                $property->enum = $args;
5✔
464
                $args = [];
5✔
465
        }
5✔
466

467

468
        protected function parseVirtualModifier(PropertyMetadata $property): void
469
        {
470
                $property->isVirtual = true;
5✔
471
        }
5✔
472

473

474
        /**
475
         * @param array<int|string, mixed> $args
476
         */
477
        protected function parseContainerModifier(PropertyMetadata $property, array &$args): void
478
        {
NEW
479
                trigger_error("Property modifier {container} is deprecated; rename it to {wrapper} modifier.", E_USER_DEPRECATED);
×
480
                $this->parseWrapperModifier($property, $args);
×
481
        }
×
482

483

484
        /**
485
         * @param array<int|string, mixed> $args
486
         */
487
        protected function parseWrapperModifier(PropertyMetadata $property, array &$args): void
488
        {
489
                $className = Reflection::expandClassName(array_shift($args), $this->currentReflection);
5✔
490
                if (!class_exists($className)) {
5✔
491
                        throw new InvalidModifierDefinitionException("Class '$className' in {wrapper} for {$this->currentReflection->name}::\${$property->name} property does not exist.");
5✔
492
                }
493
                $implements = class_implements($className);
5✔
494
                if ($implements !== false && !isset($implements[IProperty::class])) {
5✔
495
                        throw new InvalidModifierDefinitionException("Class '$className' in {wrapper} for {$this->currentReflection->name}::\${$property->name} property does not implement Nextras\\Orm\\Entity\\IProperty interface.");
5✔
496
                }
497
                $property->wrapper = $className;
5✔
498
        }
5✔
499

500

501
        /**
502
         * @param array<int|string, mixed> $args
503
         */
504
        protected function parseDefaultModifier(PropertyMetadata $property, array &$args): void
505
        {
506
                $property->defaultValue = array_shift($args);
5✔
507
        }
5✔
508

509

510
        protected function parsePrimaryModifier(PropertyMetadata $property): void
511
        {
512
                $property->isPrimary = true;
5✔
513
        }
5✔
514

515

516
        protected function parsePrimaryProxyModifier(PropertyMetadata $property): void
517
        {
518
                $property->isVirtual = true;
5✔
519
                $property->isPrimary = true;
5✔
520
                if ($property->hasGetter === null && $property->hasSetter === null) {
5✔
521
                        $property->wrapper = PrimaryProxyWrapper::class;
5✔
522
                }
523
        }
5✔
524

525

526
        protected function parseEmbeddableModifier(PropertyMetadata $property): void
527
        {
528
                if (count($property->types) !== 1) {
5✔
529
                        $num = count($property->types);
×
530
                        throw new InvalidModifierDefinitionException("Embeddable modifer requries only one class type definition, optionally nullable. $num types detected in {$this->currentReflection->name}::\${$property->name} property.");
×
531
                }
532
                $className = array_keys($property->types)[0];
5✔
533
                if (!class_exists($className)) {
5✔
534
                        throw new InvalidModifierDefinitionException("Class '$className' in {embeddable} for {$this->currentReflection->name}::\${$property->name} property does not exist.");
×
535
                }
536
                if (!is_subclass_of($className, IEmbeddable::class)) {
5✔
537
                        throw new InvalidModifierDefinitionException("Class '$className' in {embeddable} for {$this->currentReflection->name}::\${$property->name} property does not implement " . IEmbeddable::class . " interface.");
×
538
                }
539

540
                $property->wrapper = EmbeddableContainer::class;
5✔
541
                $property->args[EmbeddableContainer::class] = ['class' => $className];
5✔
542
        }
5✔
543

544

545
        protected function initPrimaryKey(): void
546
        {
547
                if ($this->reflection->isSubclassOf(IEmbeddable::class)) {
5✔
548
                        return;
5✔
549
                }
550

551
                $primaryKey = [];
5✔
552
                foreach ($this->metadata->getProperties() as $metadata) {
5✔
553
                        if ($metadata->isPrimary && !$metadata->isVirtual) {
5✔
554
                                $primaryKey[] = $metadata->name;
5✔
555
                        }
556
                }
557

558
                if (count($primaryKey) === 0) {
5✔
559
                        throw new InvalidStateException("Entity {$this->reflection->name} does not have defined any primary key.");
5✔
560
                } elseif (!$this->metadata->hasProperty('id') || !$this->metadata->getProperty('id')->isPrimary) {
5✔
561
                        throw new InvalidStateException("Entity {$this->reflection->name} has to have defined \$id property as {primary} or {primary-proxy}.");
×
562
                }
563

564
                $this->metadata->setPrimaryKey($primaryKey);
5✔
565
        }
5✔
566

567

568
        /**
569
         * @param array<int|string, mixed> $args
570
         */
571
        protected function processRelationshipEntityProperty(PropertyMetadata $property, array &$args): void
572
        {
573
                assert($property->relationship !== null);
574
                static $modifiersMap = [
5✔
575
                        PropertyRelationshipMetadata::ONE_HAS_MANY => '1:m',
2✔
576
                        PropertyRelationshipMetadata::ONE_HAS_ONE => '1:1',
2✔
577
                        PropertyRelationshipMetadata::MANY_HAS_ONE => 'm:1',
2✔
578
                        PropertyRelationshipMetadata::MANY_HAS_MANY => 'm:m',
2✔
579
                ];
580
                $modifier = $modifiersMap[$property->relationship->type];
5✔
581
                $class = array_shift($args);
5✔
582

583
                if ($class === null) {
5✔
584
                        throw new InvalidModifierDefinitionException("Relationship {{$modifier}} in {$this->currentReflection->name}::\${$property->name} has not defined target entity and its property name.");
5✔
585
                }
586

587
                $pos = strpos($class, '::');
5✔
588
                if ($pos === false) {
5✔
589
                        if (preg_match('#^[a-z0-9_\\\\]+$#i', $class) !== 1) {
5✔
590
                                throw new InvalidModifierDefinitionException("Relationship {{$modifier}} in {$this->currentReflection->name}::\${$property->name} has invalid class name of the target entity. Use Entity::\$property format.");
5✔
591
                        } elseif (!(isset($args['oneSided']) && $args['oneSided'] === true)) {
5✔
592
                                throw new InvalidModifierDefinitionException("Relationship {{$modifier}} in {$this->currentReflection->name}::\${$property->name} has not defined target property name.");
5✔
593
                        } else {
594
                                $targetProperty = null;
5✔
595
                                unset($args['oneSided']);
5✔
596
                        }
597
                } else {
598
                        $targetProperty = substr($class, $pos + 3); // skip ::$
5✔
599
                        assert($targetProperty !== false); // @phpstan-ignore-line
600
                        $class = substr($class, 0, $pos);
5✔
601

602
                        if (isset($args['oneSided'])) {
5✔
603
                                throw new InvalidModifierDefinitionException("Relationship {{$modifier}} in {$this->currentReflection->name}::\${$property->name} has set oneSided property but it also specifies target property \${$targetProperty}.");
×
604
                        }
605
                }
606

607
                /** @var class-string<IEntity> $entity */
608
                $entity = Reflection::expandClassName($class, $this->currentReflection);
5✔
609
                if (!isset($this->entityClassesMap[$entity])) {
5✔
610
                        throw new InvalidModifierDefinitionException("Relationship {{$modifier}} in {$this->currentReflection->name}::\${$property->name} points to unknown '{$entity}' entity. Don't forget to return it in IRepository::getEntityClassNames() and register its repository.");
5✔
611
                }
612

613
                $property->relationship->entity = $entity;
5✔
614
                $property->relationship->repository = $this->entityClassesMap[$entity];
5✔
615
                $property->relationship->property = $targetProperty;
5✔
616
        }
5✔
617

618

619
        /**
620
         * @param array<int|string, mixed> $args
621
         */
622
        protected function processRelationshipCascade(PropertyMetadata $property, array &$args): void
623
        {
624
                assert($property->relationship !== null);
625
                $property->relationship->cascade = $defaults = [
5✔
626
                        'persist' => false,
627
                        'remove' => false,
628
                ];
629

630
                if (!isset($args['cascade'])) {
5✔
631
                        $property->relationship->cascade['persist'] = true;
5✔
632
                        return;
5✔
633
                }
634

635
                foreach ((array) $args['cascade'] as $cascade) {
5✔
636
                        if (!isset($defaults[$cascade])) {
5✔
637
                                throw new InvalidModifierDefinitionException();
×
638
                        }
639
                        $property->relationship->cascade[$cascade] = true;
5✔
640
                }
641
                unset($args['cascade']);
5✔
642
        }
5✔
643

644

645
        /**
646
         * @param array<int|string, mixed> $args
647
         */
648
        protected function processRelationshipOrder(PropertyMetadata $property, array &$args): void
649
        {
650
                assert($property->relationship !== null);
651
                if (!isset($args['orderBy'])) {
5✔
652
                        return;
5✔
653
                }
654

655
                if (is_string($args['orderBy'])) {
5✔
656
                        $order = [$args['orderBy'] => ICollection::ASC];
5✔
657

658
                } elseif (is_array($args['orderBy']) && isset($args['orderBy'][0])) {
5✔
659
                        $order = [$args['orderBy'][0] => $args['orderBy'][1] ?? ICollection::ASC];
×
660

661
                } else {
662
                        $order = $args['orderBy'];
5✔
663
                }
664

665
                $property->relationship->order = $order;
5✔
666
                unset($args['orderBy']);
5✔
667
        }
5✔
668

669

670
        /**
671
         * @param array<int|string, mixed> $args
672
         */
673
        protected function processRelationshipExposeCollection(PropertyMetadata $property, array &$args): void
674
        {
675
                if (isset($args['exposeCollection']) && $args['exposeCollection'] === true) {
5✔
676
                        $property->args[HasMany::class]['exposeCollection'] = true;
5✔
677
                }
678
                unset($args['exposeCollection']);
5✔
679
        }
5✔
680

681

682
        /**
683
         * @param array<int|string, mixed> $args
684
         */
685
        protected function processRelationshipIsMain(PropertyMetadata $property, array &$args): void
686
        {
687
                assert($property->relationship !== null);
688
                $property->relationship->isMain = isset($args['isMain']) && $args['isMain'] === true;
5✔
689
                unset($args['isMain']);
5✔
690
        }
5✔
691
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc