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

nextras / orm / 32191453527

18 Aug 2026 10:10PM UTC coverage: 91.996% (+0.05%) from 91.945%
32191453527

Pull #826

github

web-flow
Merge f87c563b2 into aea7d1832
Pull Request #826: fix various issues

14 of 15 new or added lines in 4 files covered. (93.33%)

4356 of 4735 relevant lines covered (92.0%)

5.39 hits per line

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

92.62
/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\Extension;
22
use Nextras\Orm\Relationships\HasMany;
23
use Nextras\Orm\Relationships\ManyHasMany;
24
use Nextras\Orm\Relationships\ManyHasOne;
25
use Nextras\Orm\Relationships\OneHasMany;
26
use Nextras\Orm\Relationships\OneHasOne;
27
use Nextras\Orm\Repository\IRepository;
28
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
29
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
30
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
31
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
32
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
33
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
34
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
35
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
36
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
37
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
38
use PHPStan\PhpDocParser\Lexer\Lexer;
39
use PHPStan\PhpDocParser\Parser\ConstExprParser;
40
use PHPStan\PhpDocParser\Parser\PhpDocParser;
41
use PHPStan\PhpDocParser\Parser\TokenIterator;
42
use PHPStan\PhpDocParser\Parser\TypeParser;
43
use PHPStan\PhpDocParser\ParserConfig;
44
use ReflectionClass;
45
use function array_keys;
46
use function assert;
47
use function class_exists;
48
use function count;
49
use function is_subclass_of;
50
use function strlen;
51
use function substr;
52
use function trigger_error;
53

54

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

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

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

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

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

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

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

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

94

95
        /**
96
         * @param array<string, string> $entityClassesMap
97
         * @param array<class-string<IEntity>, class-string<IRepository<IEntity>>> $entityClassesMap
98
         * @param list<Extension> $extensions
99
         */
100
        public function __construct(
5✔
101
                array $entityClassesMap,
102
                protected array $extensions = [],
1✔
103
        )
104
        {
105
                $this->entityClassesMap = $entityClassesMap;
6✔
106
                $this->modifierParser = new ModifierParser();
6✔
107

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

123

124
        /**
125
         * Adds modifier processor.
126
         * @return static
127
         */
128
        public function addModifier(string $modifier, callable $processor)
129
        {
130
                $this->modifiers[strtolower($modifier)] = $processor;
×
131
                return $this;
×
132
        }
133

134

135
        public function parseMetadata(string $entityClass, array|null &$fileDependencies): EntityMetadata
136
        {
137
                $this->reflection = new ReflectionClass($entityClass);
6✔
138
                $this->metadata = new EntityMetadata($entityClass);
6✔
139

140
                $this->loadProperties($fileDependencies);
6✔
141
                $this->initPrimaryKey();
6✔
142

143
                foreach ($this->extensions as $extension) {
6✔
144
                        $extension->configureEntityMetadata($this->metadata);
3✔
145
                }
146

147
                if ($fileDependencies !== null) {
6✔
148
                        $fileDependencies = array_values(array_unique($fileDependencies));
6✔
149
                }
150
                return $this->metadata;
6✔
151
        }
152

153

154
        /**
155
         * @param list<string>|null $fileDependencies
156
         */
157
        protected function loadProperties(array|null &$fileDependencies): void
158
        {
159
                $classTree = [$current = $this->reflection->name];
6✔
160
                while (($current = get_parent_class($current)) !== false) {
6✔
161
                        $classTree[] = $current;
6✔
162
                }
163

164
                $methods = [];
6✔
165
                foreach ($this->reflection->getMethods() as $method) {
6✔
166
                        $methods[strtolower($method->name)] = true;
6✔
167
                }
168

169
                foreach (array_reverse($classTree) as $class) {
6✔
170
                        if (!isset($this->classPropertiesCache[$class])) {
6✔
171
                                $traits = class_uses($class);
6✔
172
                                foreach ($traits !== false ? $traits : [] as $traitName) {
6✔
173
                                        assert(trait_exists($traitName));
174
                                        $reflectionTrait = new ReflectionClass($traitName);
6✔
175
                                        $file = $reflectionTrait->getFileName();
6✔
176
                                        if ($file !== false) $fileDependencies[] = $file;
6✔
177
                                        $this->currentReflection = $reflectionTrait;
6✔
178
                                        $this->classPropertiesCache[$traitName] = $this->parseAnnotations($reflectionTrait);
6✔
179
                                }
180

181
                                $reflection = new ReflectionClass($class);
6✔
182
                                $file = $reflection->getFileName();
6✔
183
                                if ($file !== false) $fileDependencies[] = $file;
6✔
184
                                $this->currentReflection = $reflection;
6✔
185
                                $this->classPropertiesCache[$class] = $this->parseAnnotations($reflection);
6✔
186
                        }
187

188
                        $traits = class_uses($class);
6✔
189
                        foreach ($traits !== false ? $traits : [] as $traitName) {
6✔
190
                                foreach ($this->classPropertiesCache[$traitName] as $name => $property) {
6✔
191
                                        $this->metadata->setProperty($name, $this->createEntityProperty($property, $methods));
6✔
192
                                }
193
                        }
194

195
                        foreach ($this->classPropertiesCache[$class] as $name => $property) {
6✔
196
                                $this->metadata->setProperty($name, $this->createEntityProperty($property, $methods));
6✔
197
                        }
198
                }
199
        }
6✔
200

201

202
        /**
203
         * Clones a cached property metadata for a concrete entity and resolves its getters/setters.
204
         *
205
         * The parsed property metadata is cached per defining class/trait and therefore shared by all
206
         * entities that share that class/trait. Getters/setters depend on the concrete entity's method
207
         * list, so they must be resolved per entity on a private copy — otherwise the resolution baked
208
         * in for the first parsed entity would contaminate its siblings.
209
         *
210
         * @param array<string, true> $methods
211
         */
212
        protected function createEntityProperty(PropertyMetadata $property, array $methods): PropertyMetadata
213
        {
214
                $property = clone $property;
6✔
215
                $this->processPropertyGettersSetters($property, $methods);
6✔
216
                return $property;
6✔
217
        }
218

219

220
        /**
221
         * @param ReflectionClass<object> $reflection
222
         * @return array<string, PropertyMetadata>
223
         */
224
        protected function parseAnnotations(ReflectionClass $reflection): array
225
        {
226
                $docComment = $reflection->getDocComment();
6✔
227
                if ($docComment === false) return [];
6✔
228

229
                $tokens = new TokenIterator($this->phpDocLexer->tokenize($docComment));
6✔
230
                $phpDocNode = $this->phpDocParser->parse($tokens);
6✔
231

232
                $properties = [];
6✔
233
                foreach ($phpDocNode->getPropertyTagValues() as $propertyTagValue) {
6✔
234
                        $property = $this->parseProperty($propertyTagValue, $reflection->getName(), isReadonly: false);
6✔
235
                        $properties[$property->name] = $property;
6✔
236
                }
237
                foreach ($phpDocNode->getPropertyWriteTagValues() as $propertyTagValue) {
6✔
NEW
238
                        $property = $this->parseProperty($propertyTagValue, $reflection->getName(), isReadonly: false);
×
239
                        $properties[$property->name] = $property;
×
240
                }
241
                foreach ($phpDocNode->getPropertyReadTagValues() as $propertyTagValue) {
6✔
242
                        $property = $this->parseProperty($propertyTagValue, $reflection->getName(), isReadonly: true);
6✔
243
                        $properties[$property->name] = $property;
6✔
244
                }
245
                return $properties;
6✔
246
        }
247

248

249
        protected function parseProperty(
250
                PropertyTagValueNode $propertyNode,
251
                string $containerClassName,
252
                bool $isReadonly,
253
        ): PropertyMetadata
254
        {
255
                $property = new PropertyMetadata();
6✔
256
                $property->name = substr($propertyNode->propertyName, 1);
6✔
257
                $property->containerClassname = $containerClassName;
6✔
258
                $property->isReadonly = $isReadonly;
6✔
259

260
                $this->parseAnnotationTypes($property, $propertyNode->type);
6✔
261
                $this->parseAnnotationValue($property, $propertyNode->description);
6✔
262
                $this->processDefaultPropertyWrappers($property);
6✔
263

264
                foreach ($this->extensions as $extension) {
6✔
265
                        $extension->configureEntityPropertyMetadata($this->metadata, $property, $propertyNode->type);
3✔
266
                }
267

268
                return $property;
6✔
269
        }
270

271

272
        protected function parseAnnotationTypes(PropertyMetadata $property, TypeNode $type): void
273
        {
274
                static $aliases = [
6✔
275
                        'double' => 'float',
276
                        'real' => 'float',
277
                        'numeric' => 'float',
278
                        'number' => 'float',
279
                        'integer' => 'int',
280
                ];
281

282
                if ($type instanceof UnionTypeNode) {
6✔
283
                        $types = $type->types;
6✔
284
                } elseif ($type instanceof IntersectionTypeNode) {
6✔
285
                        $types = $type->types;
×
286
                } else {
287
                        $types = [$type];
6✔
288
                }
289

290
                $parsedTypes = [];
6✔
291
                foreach ($types as $subType) {
6✔
292
                        if ($subType instanceof NullableTypeNode) {
6✔
293
                                $property->isNullable = true;
6✔
294
                                $subType = $subType->type;
6✔
295
                        }
296
                        if ($subType instanceof GenericTypeNode) {
6✔
297
                                $subType = $subType->type;
6✔
298
                        }
299

300
                        if ($subType instanceof IdentifierTypeNode) {
6✔
301
                                $subTypeName = $subType->name;
6✔
302
                                if ($subTypeName === 'boolean') $subTypeName = 'bool'; // avoid expansion, bug in Nette
6✔
303
                                $expandedSubType = Reflection::expandClassName($subTypeName, $this->currentReflection);
6✔
304
                                $expandedSubTypeLower = strtolower($expandedSubType);
6✔
305

306
                                if ($expandedSubTypeLower === 'null') {
6✔
307
                                        $property->isNullable = true;
6✔
308
                                        continue;
6✔
309
                                }
310
                                if ($expandedSubType === DateTime::class || is_subclass_of($expandedSubType, DateTime::class)) {
6✔
311
                                        throw new NotSupportedException("Type '{$expandedSubType}' in {$this->currentReflection->name}::\${$property->name} property is not supported anymore. Use \DateTimeImmutable or \Nextras\Dbal\Utils\DateTimeImmutable type.");
×
312
                                }
313
                                if (isset($aliases[$expandedSubTypeLower])) {
6✔
314
                                        /** @var string $expandedSubType */
315
                                        $expandedSubType = $aliases[$expandedSubTypeLower];
×
316
                                }
317
                                $parsedTypes[$expandedSubType] = true;
6✔
318
                        } elseif ($subType instanceof ArrayTypeNode) {
6✔
319
                                $parsedTypes['array'] = true;
6✔
320
                        } elseif ($subType instanceof ArrayShapeNode) {
6✔
321
                                $parsedTypes['array'] = true;
6✔
322
                        } elseif ($subType instanceof ObjectShapeNode) {
×
323
                                $parsedTypes['object'] = true;
×
324
                        } else {
325
                                throw new NotSupportedException("Type '{$type}' in {$this->currentReflection->name}::\${$property->name} property is not supported. For Nextras Orm purpose simplify it.");
×
326
                        }
327
                }
328

329
                if (count($parsedTypes) < 1) {
6✔
330
                        throw new NotSupportedException("Property {$this->currentReflection->name}::\${$property->name} without a type definition is not supported.");
×
331
                }
332
                $property->types = $parsedTypes;
6✔
333
        }
6✔
334

335

336
        protected function parseAnnotationValue(PropertyMetadata $property, string $propertyComment): void
337
        {
338
                if (strlen($propertyComment) === 0) {
6✔
339
                        return;
6✔
340
                }
341

342
                $matches = $this->modifierParser->matchModifiers($propertyComment);
6✔
343
                foreach ($matches as $macroContent) {
6✔
344
                        try {
345
                                $args = $this->modifierParser->parse($macroContent, $this->currentReflection);
6✔
346
                        } catch (InvalidModifierDefinitionException $e) {
6✔
347
                                throw new InvalidModifierDefinitionException(
6✔
348
                                        "Invalid modifier definition for {$this->currentReflection->name}::\${$property->name} property.",
6✔
349
                                        0,
6✔
350
                                        $e,
351
                                );
352
                        }
353
                        $this->processPropertyModifier($property, $args[0], $args[1]);
6✔
354
                }
355
        }
6✔
356

357

358
        /**
359
         * @param array<string, true> $methods
360
         */
361
        protected function processPropertyGettersSetters(PropertyMetadata $property, array $methods): void
362
        {
363
                $getter = 'getter' . strtolower($property->name);
6✔
364
                if (isset($methods[$getter])) {
6✔
365
                        $property->hasGetter = $getter;
6✔
366
                }
367
                $setter = 'setter' . strtolower($property->name);
6✔
368
                if (isset($methods[$setter])) {
6✔
369
                        $property->hasSetter = $setter;
6✔
370
                }
371
        }
6✔
372

373

374
        protected function processDefaultPropertyWrappers(PropertyMetadata $property): void
375
        {
376
                if ($property->wrapper !== null) return;
6✔
377
                if ($property->isVirtual) return;
6✔
378

379
                foreach ($property->types as $type => $_) {
6✔
380
                        if (is_subclass_of($type, \DateTimeImmutable::class) || $type === \DateTimeImmutable::class) {
6✔
381
                                $property->wrapper = DateTimeWrapper::class;
6✔
382
                        } elseif (is_subclass_of($type, BackedEnum::class)) {
6✔
383
                                $property->wrapper = BackedEnumWrapper::class;
6✔
384
                        }
385
                }
386
        }
6✔
387

388

389
        /**
390
         * @param array<int|string, mixed> $args
391
         */
392
        protected function processPropertyModifier(PropertyMetadata $property, string $modifier, array $args): void
393
        {
394
                $type = strtolower($modifier);
6✔
395
                if (!isset($this->modifiers[$type])) {
6✔
396
                        throw new InvalidModifierDefinitionException(
6✔
397
                                "Unknown modifier '$type' type for {$this->currentReflection->name}::\${$property->name} property.",
6✔
398
                        );
399
                }
400

401
                $callback = $this->modifiers[$type];
6✔
402
                if (!is_array($callback)) {
6✔
403
                        $callback = [$this, $callback];
6✔
404
                }
405
                assert(is_callable($callback));
406
                call_user_func_array($callback, [$property, &$args]);
6✔
407
                if (count($args) > 0) {
6✔
408
                        $parts = [];
6✔
409
                        foreach ($args as $key => $val) {
6✔
410
                                if (is_numeric($key) && !is_array($val)) {
6✔
411
                                        $parts[] = $val;
6✔
412
                                        continue;
6✔
413
                                }
414
                                $parts[] = $key;
6✔
415
                        }
416
                        throw new InvalidModifierDefinitionException(
6✔
417
                                "Modifier {{$type}} in {$this->currentReflection->name}::\${$property->name} property has unknown arguments: " . implode(', ', $parts) . '.',
6✔
418
                        );
419
                }
420
        }
6✔
421

422

423
        /**
424
         * @param array<int|string, mixed> $args
425
         */
426
        protected function parseOneHasOneModifier(PropertyMetadata $property, array &$args): void
427
        {
428
                $property->relationship = new PropertyRelationshipMetadata();
6✔
429
                $property->relationship->type = PropertyRelationshipMetadata::ONE_HAS_ONE;
6✔
430
                $property->wrapper = OneHasOne::class;
6✔
431
                $this->processRelationshipIsMain($property, $args);
6✔
432
                $this->processRelationshipEntityProperty($property, $args);
6✔
433
                $this->processRelationshipCascade($property, $args);
6✔
434
                assert($property->relationship !== null);
435
                $property->isVirtual = !$property->relationship->isMain;
6✔
436
        }
6✔
437

438

439
        /**
440
         * @param array<int|string, mixed> $args
441
         */
442
        protected function parseOneHasManyModifier(PropertyMetadata $property, array &$args): void
443
        {
444
                $property->relationship = new PropertyRelationshipMetadata();
6✔
445
                $property->relationship->type = PropertyRelationshipMetadata::ONE_HAS_MANY;
6✔
446
                $property->wrapper = OneHasMany::class;
6✔
447
                $property->isVirtual = true;
6✔
448
                $this->processRelationshipEntityProperty($property, $args);
6✔
449
                $this->processRelationshipCascade($property, $args);
6✔
450
                $this->processRelationshipOrder($property, $args);
6✔
451
                $this->processRelationshipExposeCollection($property, $args);
6✔
452
        }
6✔
453

454

455
        /**
456
         * @param array<int|string, mixed> $args
457
         */
458
        protected function parseManyHasOneModifier(PropertyMetadata $property, array &$args): void
459
        {
460
                $property->relationship = new PropertyRelationshipMetadata();
6✔
461
                $property->relationship->type = PropertyRelationshipMetadata::MANY_HAS_ONE;
6✔
462
                $property->wrapper = ManyHasOne::class;
6✔
463
                $this->processRelationshipEntityProperty($property, $args);
6✔
464
                $this->processRelationshipCascade($property, $args);
6✔
465
        }
6✔
466

467

468
        /**
469
         * @param array<int|string, mixed> $args
470
         */
471
        protected function parseManyHasManyModifier(PropertyMetadata $property, array &$args): void
472
        {
473
                $property->relationship = new PropertyRelationshipMetadata();
6✔
474
                $property->relationship->type = PropertyRelationshipMetadata::MANY_HAS_MANY;
6✔
475
                $property->wrapper = ManyHasMany::class;
6✔
476
                $property->isVirtual = true;
6✔
477
                $this->processRelationshipIsMain($property, $args);
6✔
478
                $this->processRelationshipEntityProperty($property, $args);
6✔
479
                $this->processRelationshipCascade($property, $args);
6✔
480
                $this->processRelationshipOrder($property, $args);
6✔
481
                $this->processRelationshipExposeCollection($property, $args);
6✔
482
        }
6✔
483

484

485
        /**
486
         * @param array<int|string, mixed> $args
487
         */
488
        protected function parseEnumModifier(PropertyMetadata $property, array &$args): void
489
        {
490
                $property->enum = $args;
6✔
491
                $args = [];
6✔
492
        }
6✔
493

494

495
        protected function parseVirtualModifier(PropertyMetadata $property): void
496
        {
497
                $property->isVirtual = true;
6✔
498
        }
6✔
499

500

501
        /**
502
         * @param array<int|string, mixed> $args
503
         */
504
        protected function parseContainerModifier(PropertyMetadata $property, array &$args): void
505
        {
506
                trigger_error("Property modifier {container} is deprecated; rename it to {wrapper} modifier.", E_USER_DEPRECATED);
×
507
                $this->parseWrapperModifier($property, $args);
×
508
        }
×
509

510

511
        /**
512
         * @param array<int|string, mixed> $args
513
         */
514
        protected function parseWrapperModifier(PropertyMetadata $property, array &$args): void
515
        {
516
                $className = Reflection::expandClassName(array_shift($args), $this->currentReflection);
6✔
517
                if (!class_exists($className)) {
6✔
518
                        throw new InvalidModifierDefinitionException("Class '$className' in {wrapper} for {$this->currentReflection->name}::\${$property->name} property does not exist.");
6✔
519
                }
520
                $implements = class_implements($className);
6✔
521
                if ($implements !== false && !isset($implements[IProperty::class])) {
6✔
522
                        throw new InvalidModifierDefinitionException("Class '$className' in {wrapper} for {$this->currentReflection->name}::\${$property->name} property does not implement Nextras\\Orm\\Entity\\IProperty interface.");
6✔
523
                }
524
                $property->wrapper = $className;
6✔
525
        }
6✔
526

527

528
        /**
529
         * @param array<int|string, mixed> $args
530
         */
531
        protected function parseDefaultModifier(PropertyMetadata $property, array &$args): void
532
        {
533
                $property->defaultValue = array_shift($args);
6✔
534
        }
6✔
535

536

537
        protected function parsePrimaryModifier(PropertyMetadata $property): void
538
        {
539
                $property->isPrimary = true;
6✔
540
        }
6✔
541

542

543
        protected function parsePrimaryProxyModifier(PropertyMetadata $property): void
544
        {
545
                $property->isVirtual = true;
6✔
546
                $property->isPrimary = true;
6✔
547
                if ($property->hasGetter === null && $property->hasSetter === null) {
6✔
548
                        $property->wrapper = PrimaryProxyWrapper::class;
6✔
549
                }
550
        }
6✔
551

552

553
        protected function parseEmbeddableModifier(PropertyMetadata $property): void
554
        {
555
                if (count($property->types) !== 1) {
6✔
556
                        $num = count($property->types);
×
557
                        throw new InvalidModifierDefinitionException("Embeddable modifer requries only one class type definition, optionally nullable. $num types detected in {$this->currentReflection->name}::\${$property->name} property.");
×
558
                }
559
                $className = array_keys($property->types)[0];
6✔
560
                if (!class_exists($className)) {
6✔
561
                        throw new InvalidModifierDefinitionException("Class '$className' in {embeddable} for {$this->currentReflection->name}::\${$property->name} property does not exist.");
×
562
                }
563
                if (!is_subclass_of($className, IEmbeddable::class)) {
6✔
564
                        throw new InvalidModifierDefinitionException("Class '$className' in {embeddable} for {$this->currentReflection->name}::\${$property->name} property does not implement " . IEmbeddable::class . " interface.");
×
565
                }
566

567
                $property->wrapper = EmbeddableContainer::class;
6✔
568
                $property->args[EmbeddableContainer::class] = ['class' => $className];
6✔
569
        }
6✔
570

571

572
        protected function initPrimaryKey(): void
573
        {
574
                if ($this->reflection->isSubclassOf(IEmbeddable::class)) {
6✔
575
                        return;
6✔
576
                }
577

578
                $primaryKey = [];
6✔
579
                foreach ($this->metadata->getProperties() as $metadata) {
6✔
580
                        if ($metadata->isPrimary && !$metadata->isVirtual) {
6✔
581
                                $primaryKey[] = $metadata->name;
6✔
582
                        }
583
                }
584

585
                if (count($primaryKey) === 0) {
6✔
586
                        throw new InvalidStateException("Entity {$this->reflection->name} does not have defined any primary key.");
6✔
587
                } elseif (!$this->metadata->hasProperty('id') || !$this->metadata->getProperty('id')->isPrimary) {
6✔
588
                        throw new InvalidStateException("Entity {$this->reflection->name} has to have defined \$id property as {primary} or {primary-proxy}.");
×
589
                }
590

591
                $this->metadata->setPrimaryKey($primaryKey);
6✔
592
        }
6✔
593

594

595
        /**
596
         * @param array<int|string, mixed> $args
597
         */
598
        protected function processRelationshipEntityProperty(PropertyMetadata $property, array &$args): void
599
        {
600
                assert($property->relationship !== null);
601
                static $modifiersMap = [
6✔
602
                        PropertyRelationshipMetadata::ONE_HAS_MANY => '1:m',
3✔
603
                        PropertyRelationshipMetadata::ONE_HAS_ONE => '1:1',
3✔
604
                        PropertyRelationshipMetadata::MANY_HAS_ONE => 'm:1',
3✔
605
                        PropertyRelationshipMetadata::MANY_HAS_MANY => 'm:m',
3✔
606
                ];
607
                $modifier = $modifiersMap[$property->relationship->type];
6✔
608
                $class = array_shift($args);
6✔
609

610
                if ($class === null) {
6✔
611
                        throw new InvalidModifierDefinitionException("Relationship {{$modifier}} in {$this->currentReflection->name}::\${$property->name} has not defined target entity and its property name.");
6✔
612
                }
613

614
                $pos = strpos($class, '::');
6✔
615
                if ($pos === false) {
6✔
616
                        if (preg_match('#^[a-z0-9_\\\\]+$#i', $class) !== 1) {
6✔
617
                                throw new InvalidModifierDefinitionException("Relationship {{$modifier}} in {$this->currentReflection->name}::\${$property->name} has invalid class name of the target entity. Use Entity::\$property format.");
6✔
618
                        } elseif (!(isset($args['oneSided']) && $args['oneSided'] === true)) {
6✔
619
                                throw new InvalidModifierDefinitionException("Relationship {{$modifier}} in {$this->currentReflection->name}::\${$property->name} has not defined target property name.");
6✔
620
                        } else {
621
                                $targetProperty = null;
6✔
622
                                unset($args['oneSided']);
6✔
623
                        }
624
                } else {
625
                        $targetProperty = substr($class, $pos + 3); // skip ::$
6✔
626
                        assert($targetProperty !== false); // @phpstan-ignore-line
627
                        $class = substr($class, 0, $pos);
6✔
628

629
                        if (isset($args['oneSided'])) {
6✔
630
                                throw new InvalidModifierDefinitionException("Relationship {{$modifier}} in {$this->currentReflection->name}::\${$property->name} has set oneSided property but it also specifies target property \${$targetProperty}.");
×
631
                        }
632
                }
633

634
                /** @var class-string<IEntity> $entity */
635
                $entity = Reflection::expandClassName($class, $this->currentReflection);
6✔
636
                if (!isset($this->entityClassesMap[$entity])) {
6✔
637
                        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.");
6✔
638
                }
639

640
                $property->relationship->entity = $entity;
6✔
641
                $property->relationship->repository = $this->entityClassesMap[$entity];
6✔
642
                $property->relationship->property = $targetProperty;
6✔
643
        }
6✔
644

645

646
        /**
647
         * @param array<int|string, mixed> $args
648
         */
649
        protected function processRelationshipCascade(PropertyMetadata $property, array &$args): void
650
        {
651
                assert($property->relationship !== null);
652
                $property->relationship->cascade = $defaults = [
6✔
653
                        'persist' => false,
654
                        'remove' => false,
655
                        'removeOrphan' => false,
656
                ];
657

658
                if (!isset($args['cascade'])) {
6✔
659
                        $property->relationship->cascade['persist'] = true;
6✔
660
                        return;
6✔
661
                }
662

663
                foreach ((array) $args['cascade'] as $cascade) {
6✔
664
                        if (!isset($defaults[$cascade])) {
6✔
665
                                throw new InvalidModifierDefinitionException();
×
666
                        }
667
                        $property->relationship->cascade[$cascade] = true;
6✔
668
                }
669
                unset($args['cascade']);
6✔
670
        }
6✔
671

672

673
        /**
674
         * @param array<int|string, mixed> $args
675
         */
676
        protected function processRelationshipOrder(PropertyMetadata $property, array &$args): void
677
        {
678
                assert($property->relationship !== null);
679
                if (!isset($args['orderBy'])) {
6✔
680
                        return;
6✔
681
                }
682

683
                if (is_string($args['orderBy'])) {
6✔
684
                        $order = [$args['orderBy'] => ICollection::ASC];
6✔
685

686
                } elseif (is_array($args['orderBy']) && isset($args['orderBy'][0])) {
6✔
687
                        $order = [$args['orderBy'][0] => $args['orderBy'][1] ?? ICollection::ASC];
×
688

689
                } else {
690
                        $order = $args['orderBy'];
6✔
691
                }
692

693
                $property->relationship->order = $order;
6✔
694
                unset($args['orderBy']);
6✔
695
        }
6✔
696

697

698
        /**
699
         * @param array<int|string, mixed> $args
700
         */
701
        protected function processRelationshipExposeCollection(PropertyMetadata $property, array &$args): void
702
        {
703
                if (isset($args['exposeCollection']) && $args['exposeCollection'] === true) {
6✔
704
                        $property->args[HasMany::class]['exposeCollection'] = true;
6✔
705
                }
706
                unset($args['exposeCollection']);
6✔
707
        }
6✔
708

709

710
        /**
711
         * @param array<int|string, mixed> $args
712
         */
713
        protected function processRelationshipIsMain(PropertyMetadata $property, array &$args): void
714
        {
715
                assert($property->relationship !== null);
716
                $property->relationship->isMain = isset($args['isMain']) && $args['isMain'] === true;
6✔
717
                unset($args['isMain']);
6✔
718
        }
6✔
719
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc