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

nextras / orm / 40780907175

28 Oct 2026 10:33PM UTC coverage: 92.01% (+0.01%) from 91.998%
40780907175

Pull #842

github

web-flow
Merge 6ce4bf857 into 10fe91984
Pull Request #842: Setup DressCode

253 of 306 new or added lines in 64 files covered. (82.68%)

13 existing lines in 9 files now uncovered.

4399 of 4781 relevant lines covered (92.01%)

5.4 hits per line

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

91.97
/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 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<*>>> */
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<*>>> $entityClassesMap
97
         */
98
        public function __construct(
5 ✔
99
                array $entityClassesMap,
100
                /** @var list<Extension> */
101
                protected array $extensions = [],
1 ✔
102
        )
103
        {
104
                $this->entityClassesMap = $entityClassesMap;
6 ✔
105
                $this->modifierParser = new ModifierParser();
6 ✔
106

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

122

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

133

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

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

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

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

152

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

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

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

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

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

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

200

201
        /**
202
         * @param ReflectionClass<object> $reflection
203
         * @param array<string, true> $methods
204
         * @return array<string, PropertyMetadata>
205
         */
206
        protected function parseAnnotations(ReflectionClass $reflection, array $methods): array
207
        {
208
                $docComment = $reflection->getDocComment();
6 ✔
209
                if ($docComment === false) return [];
6 ✔
210

211
                $tokens = new TokenIterator($this->phpDocLexer->tokenize($docComment));
6 ✔
212
                $phpDocNode = $this->phpDocParser->parse($tokens);
6 ✔
213

214
                $properties = [];
6 ✔
215
                foreach ($phpDocNode->getPropertyTagValues() as $propertyTagValue) {
6 ✔
216
                        $property = $this->parseProperty($propertyTagValue, $reflection->getName(), $methods, isReadonly: false);
6 ✔
217
                        $properties[$property->name] = $property;
6 ✔
218
                }
219
                foreach ($phpDocNode->getPropertyWriteTagValues() as $propertyTagValue) {
6 ✔
220
                        $property = $this->parseProperty($propertyTagValue, $reflection->getName(), $methods, isReadonly: false);
×
221
                        $properties[$property->name] = $property;
×
222
                }
223
                foreach ($phpDocNode->getPropertyReadTagValues() as $propertyTagValue) {
6 ✔
224
                        $property = $this->parseProperty($propertyTagValue, $reflection->getName(), $methods, isReadonly: true);
6 ✔
225
                        $properties[$property->name] = $property;
6 ✔
226
                }
227
                return $properties;
6 ✔
228
        }
229

230

231
        /**
232
         * @param array<string, true> $methods
233
         */
234
        protected function parseProperty(
235
                PropertyTagValueNode $propertyNode,
236
                string $containerClassName,
237
                array $methods,
238
                bool $isReadonly,
239
        ): PropertyMetadata
240
        {
241
                $property = new PropertyMetadata();
6 ✔
242
                $property->name = substr($propertyNode->propertyName, 1);
6 ✔
243
                $property->containerClassname = $containerClassName;
6 ✔
244
                $property->isReadonly = $isReadonly;
6 ✔
245

246
                $this->parseAnnotationTypes($property, $propertyNode->type);
6 ✔
247
                $this->parseAnnotationValue($property, $propertyNode->description);
6 ✔
248
                $this->processPropertyGettersSetters($property, $methods);
6 ✔
249
                $this->processDefaultPropertyWrappers($property);
6 ✔
250

251
                foreach ($this->extensions as $extension) {
6 ✔
252
                        $extension->configureEntityPropertyMetadata($this->metadata, $property, $propertyNode->type);
2 ✔
253
                }
254

255
                return $property;
6 ✔
256
        }
257

258

259
        protected function parseAnnotationTypes(PropertyMetadata $property, TypeNode $type): void
260
        {
261
                static $aliases = [
6 ✔
262
                        'double' => 'float',
263
                        'real' => 'float',
264
                        'numeric' => 'float',
265
                        'number' => 'float',
266
                        'integer' => 'int',
267
                ];
268

269
                if ($type instanceof UnionTypeNode) {
6 ✔
270
                        $types = $type->types;
6 ✔
271
                } elseif ($type instanceof IntersectionTypeNode) {
6 ✔
272
                        $types = $type->types;
×
273
                } else {
274
                        $types = [$type];
6 ✔
275
                }
276

277
                $parsedTypes = [];
6 ✔
278
                foreach ($types as $subType) {
6 ✔
279
                        if ($subType instanceof NullableTypeNode) {
6 ✔
280
                                $property->isNullable = true;
6 ✔
281
                                $subType = $subType->type;
6 ✔
282
                        }
283
                        if ($subType instanceof GenericTypeNode) {
6 ✔
284
                                $subType = $subType->type;
6 ✔
285
                        }
286

287
                        if ($subType instanceof IdentifierTypeNode) {
6 ✔
288
                                $subTypeName = $subType->name;
6 ✔
289
                                if ($subTypeName === 'boolean') $subTypeName = 'bool'; // avoid expansion, bug in Nette
6 ✔
290
                                $expandedSubType = Reflection::expandClassName($subTypeName, $this->currentReflection);
6 ✔
291
                                $expandedSubTypeLower = strtolower($expandedSubType);
6 ✔
292

293
                                if ($expandedSubTypeLower === 'null') {
6 ✔
294
                                        $property->isNullable = true;
6 ✔
295
                                        continue;
6 ✔
296
                                }
297
                                if ($expandedSubType === DateTime::class || is_subclass_of($expandedSubType, DateTime::class)) {
6 ✔
NEW
298
                                        throw new NotSupportedException("Type '{$expandedSubType}' in {$this->currentReflection->name}::\${$property->name} property is not supported anymore. Use \\DateTimeImmutable or \\Nextras\\Dbal\\Utils\\DateTimeImmutable type.");
×
299
                                }
300
                                if (isset($aliases[$expandedSubTypeLower])) {
6 ✔
301
                                        /** @var string $expandedSubType */
302
                                        $expandedSubType = $aliases[$expandedSubTypeLower];
×
303
                                }
304
                                $parsedTypes[$expandedSubType] = true;
6 ✔
305
                        } elseif ($subType instanceof ArrayTypeNode) {
6 ✔
306
                                $parsedTypes['array'] = true;
6 ✔
307
                        } elseif ($subType instanceof ArrayShapeNode) {
6 ✔
308
                                $parsedTypes['array'] = true;
6 ✔
309
                        } elseif ($subType instanceof ObjectShapeNode) {
×
310
                                $parsedTypes['object'] = true;
×
311
                        } else {
312
                                throw new NotSupportedException("Type '{$type}' in {$this->currentReflection->name}::\${$property->name} property is not supported. For Nextras Orm purpose simplify it.");
×
313
                        }
314
                }
315

316
                if (count($parsedTypes) < 1) {
6 ✔
317
                        throw new NotSupportedException("Property {$this->currentReflection->name}::\${$property->name} without a type definition is not supported.");
×
318
                }
319
                $property->types = $parsedTypes;
6 ✔
320
        }
6 ✔
321

322

323
        protected function parseAnnotationValue(PropertyMetadata $property, string $propertyComment): void
324
        {
325
                if ($propertyComment === '') {
6 ✔
326
                        return;
6 ✔
327
                }
328

329
                $matches = $this->modifierParser->matchModifiers($propertyComment);
6 ✔
330
                foreach ($matches as $macroContent) {
6 ✔
331
                        try {
332
                                $args = $this->modifierParser->parse($macroContent, $this->currentReflection);
6 ✔
333
                        } catch (InvalidModifierDefinitionException $e) {
6 ✔
334
                                throw new InvalidModifierDefinitionException(
6 ✔
335
                                        "Invalid modifier definition for {$this->currentReflection->name}::\${$property->name} property.",
6 ✔
336
                                        0,
6 ✔
337
                                        $e,
338
                                );
339
                        }
340
                        $this->processPropertyModifier($property, $args[0], $args[1]);
6 ✔
341
                }
342
        }
6 ✔
343

344

345
        /**
346
         * @param array<string, true> $methods
347
         */
348
        protected function processPropertyGettersSetters(PropertyMetadata $property, array $methods): void
349
        {
350
                $getter = 'getter' . strtolower($property->name);
6 ✔
351
                if (isset($methods[$getter])) {
6 ✔
352
                        $property->hasGetter = $getter;
6 ✔
353
                }
354
                $setter = 'setter' . strtolower($property->name);
6 ✔
355
                if (isset($methods[$setter])) {
6 ✔
356
                        $property->hasSetter = $setter;
×
357
                }
358
        }
6 ✔
359

360

361
        protected function processDefaultPropertyWrappers(PropertyMetadata $property): void
362
        {
363
                if ($property->wrapper !== null) return;
6 ✔
364
                if ($property->isVirtual) return;
6 ✔
365

366
                foreach ($property->types as $type => $_) {
6 ✔
367
                        if (is_subclass_of($type, \DateTimeImmutable::class) || $type === \DateTimeImmutable::class) {
6 ✔
368
                                $property->wrapper = DateTimeWrapper::class;
6 ✔
369
                        } elseif (is_subclass_of($type, BackedEnum::class)) {
6 ✔
370
                                $property->wrapper = BackedEnumWrapper::class;
6 ✔
371
                        }
372
                }
373
        }
6 ✔
374

375

376
        /**
377
         * @param array<int|string, mixed> $args
378
         */
379
        protected function processPropertyModifier(PropertyMetadata $property, string $modifier, array $args): void
380
        {
381
                $type = strtolower($modifier);
6 ✔
382
                if (!isset($this->modifiers[$type])) {
6 ✔
383
                        throw new InvalidModifierDefinitionException(
6 ✔
384
                                "Unknown modifier '$type' type for {$this->currentReflection->name}::\${$property->name} property.",
6 ✔
385
                        );
386
                }
387

388
                $callback = $this->modifiers[$type];
6 ✔
389
                if (!is_array($callback)) {
6 ✔
390
                        $callback = [$this, $callback];
6 ✔
391
                }
392
                assert(is_callable($callback));
393
                call_user_func_array($callback, [$property, &$args]);
6 ✔
394
                if (count($args) > 0) {
6 ✔
395
                        $parts = [];
6 ✔
396
                        foreach ($args as $key => $val) {
6 ✔
397
                                if (is_numeric($key) && !is_array($val)) {
6 ✔
398
                                        $parts[] = $val;
6 ✔
399
                                        continue;
6 ✔
400
                                }
401
                                $parts[] = $key;
6 ✔
402
                        }
403
                        throw new InvalidModifierDefinitionException(
6 ✔
404
                                "Modifier {{$type}} in {$this->currentReflection->name}::\${$property->name} property has unknown arguments: "
6 ✔
405
                                . implode(', ', $parts)
6 ✔
406
                                . '.',
6 ✔
407
                        );
408
                }
409
        }
6 ✔
410

411

412
        /**
413
         * @param array<int|string, mixed> $args
414
         */
415
        protected function parseOneHasOneModifier(PropertyMetadata $property, array &$args): void
416
        {
417
                $property->relationship = new PropertyRelationshipMetadata();
6 ✔
418
                $property->relationship->type = PropertyRelationshipMetadata::ONE_HAS_ONE;
6 ✔
419
                $property->wrapper = OneHasOne::class;
6 ✔
420
                $this->processRelationshipIsMain($property, $args);
6 ✔
421
                $this->processRelationshipEntityProperty($property, $args);
6 ✔
422
                $this->processRelationshipCascade($property, $args);
6 ✔
423
                assert($property->relationship !== null);
424
                $property->isVirtual = !$property->relationship->isMain;
6 ✔
425
        }
6 ✔
426

427

428
        /**
429
         * @param array<int|string, mixed> $args
430
         */
431
        protected function parseOneHasManyModifier(PropertyMetadata $property, array &$args): void
432
        {
433
                $property->relationship = new PropertyRelationshipMetadata();
6 ✔
434
                $property->relationship->type = PropertyRelationshipMetadata::ONE_HAS_MANY;
6 ✔
435
                $property->wrapper = OneHasMany::class;
6 ✔
436
                $property->isVirtual = true;
6 ✔
437
                $this->processRelationshipEntityProperty($property, $args);
6 ✔
438
                $this->processRelationshipCascade($property, $args);
6 ✔
439
                $this->processRelationshipOrder($property, $args);
6 ✔
440
                $this->processRelationshipExposeCollection($property, $args);
6 ✔
441
        }
6 ✔
442

443

444
        /**
445
         * @param array<int|string, mixed> $args
446
         */
447
        protected function parseManyHasOneModifier(PropertyMetadata $property, array &$args): void
448
        {
449
                $property->relationship = new PropertyRelationshipMetadata();
6 ✔
450
                $property->relationship->type = PropertyRelationshipMetadata::MANY_HAS_ONE;
6 ✔
451
                $property->wrapper = ManyHasOne::class;
6 ✔
452
                $this->processRelationshipEntityProperty($property, $args);
6 ✔
453
                $this->processRelationshipCascade($property, $args);
6 ✔
454
        }
6 ✔
455

456

457
        /**
458
         * @param array<int|string, mixed> $args
459
         */
460
        protected function parseManyHasManyModifier(PropertyMetadata $property, array &$args): void
461
        {
462
                $property->relationship = new PropertyRelationshipMetadata();
6 ✔
463
                $property->relationship->type = PropertyRelationshipMetadata::MANY_HAS_MANY;
6 ✔
464
                $property->wrapper = ManyHasMany::class;
6 ✔
465
                $property->isVirtual = true;
6 ✔
466
                $this->processRelationshipIsMain($property, $args);
6 ✔
467
                $this->processRelationshipEntityProperty($property, $args);
6 ✔
468
                $this->processRelationshipCascade($property, $args);
6 ✔
469
                $this->processRelationshipOrder($property, $args);
6 ✔
470
                $this->processRelationshipExposeCollection($property, $args);
6 ✔
471
        }
6 ✔
472

473

474
        /**
475
         * @param array<int|string, mixed> $args
476
         */
477
        protected function parseEnumModifier(PropertyMetadata $property, array &$args): void
478
        {
479
                $property->enum = $args;
6 ✔
480
                $args = [];
6 ✔
481
        }
6 ✔
482

483

484
        protected function parseVirtualModifier(PropertyMetadata $property): void
485
        {
486
                $property->isVirtual = true;
6 ✔
487
        }
6 ✔
488

489

490
        /**
491
         * @param array<int|string, mixed> $args
492
         */
493
        protected function parseContainerModifier(PropertyMetadata $property, array &$args): void
494
        {
NEW
495
                trigger_error(
×
496
                        'Property modifier {container} is deprecated; rename it to {wrapper} modifier.',
NEW
497
                        E_USER_DEPRECATED,
×
498
                );
499
                $this->parseWrapperModifier($property, $args);
×
500
        }
×
501

502

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

519

520
        /**
521
         * @param array<int|string, mixed> $args
522
         */
523
        protected function parseDefaultModifier(PropertyMetadata $property, array &$args): void
524
        {
525
                $property->defaultValue = array_shift($args);
6 ✔
526
        }
6 ✔
527

528

529
        protected function parsePrimaryModifier(PropertyMetadata $property): void
530
        {
531
                $property->isPrimary = true;
6 ✔
532
        }
6 ✔
533

534

535
        protected function parsePrimaryProxyModifier(PropertyMetadata $property): void
536
        {
537
                $property->isVirtual = true;
6 ✔
538
                $property->isPrimary = true;
6 ✔
539
                if ($property->hasGetter === null && $property->hasSetter === null) {
6 ✔
540
                        $property->wrapper = PrimaryProxyWrapper::class;
6 ✔
541
                }
542
        }
6 ✔
543

544

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

559
                $property->wrapper = EmbeddableContainer::class;
6 ✔
560
                $property->args[EmbeddableContainer::class] = ['class' => $className];
6 ✔
561
        }
6 ✔
562

563

564
        protected function initPrimaryKey(): void
565
        {
566
                if ($this->reflection->isSubclassOf(IEmbeddable::class)) {
6 ✔
567
                        return;
6 ✔
568
                }
569

570
                $primaryKey = [];
6 ✔
571
                foreach ($this->metadata->getProperties() as $metadata) {
6 ✔
572
                        if ($metadata->isPrimary && !$metadata->isVirtual) {
6 ✔
573
                                $primaryKey[] = $metadata->name;
6 ✔
574
                        }
575
                }
576

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

583
                $this->metadata->setPrimaryKey($primaryKey);
6 ✔
584
        }
6 ✔
585

586

587
        /**
588
         * @param array<int|string, mixed> $args
589
         */
590
        protected function processRelationshipEntityProperty(PropertyMetadata $property, array &$args): void
591
        {
592
                assert($property->relationship !== null);
593
                static $modifiersMap = [
6 ✔
594
                        PropertyRelationshipMetadata::ONE_HAS_MANY => '1:m',
3 ✔
595
                        PropertyRelationshipMetadata::ONE_HAS_ONE => '1:1',
3 ✔
596
                        PropertyRelationshipMetadata::MANY_HAS_ONE => 'm:1',
3 ✔
597
                        PropertyRelationshipMetadata::MANY_HAS_MANY => 'm:m',
3 ✔
598
                ];
599
                $modifier = $modifiersMap[$property->relationship->type];
6 ✔
600
                $class = array_shift($args);
6 ✔
601

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

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

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

626
                /** @var class-string<IEntity> $entity */
627
                $entity = Reflection::expandClassName($class, $this->currentReflection);
6 ✔
628
                if (!isset($this->entityClassesMap[$entity])) {
6 ✔
629
                        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 ✔
630
                }
631

632
                $property->relationship->entity = $entity;
6 ✔
633
                $property->relationship->repository = $this->entityClassesMap[$entity];
6 ✔
634
                $property->relationship->property = $targetProperty;
6 ✔
635
        }
6 ✔
636

637

638
        /**
639
         * @param array<int|string, mixed> $args
640
         */
641
        protected function processRelationshipCascade(PropertyMetadata $property, array &$args): void
642
        {
643
                assert($property->relationship !== null);
644
                $property->relationship->cascade = $defaults = [
6 ✔
645
                        'persist' => false,
646
                        'remove' => false,
647
                        'removeOrphan' => false,
648
                ];
649

650
                if (!isset($args['cascade'])) {
6 ✔
651
                        $property->relationship->cascade['persist'] = true;
6 ✔
652
                        return;
6 ✔
653
                }
654

655
                foreach ((array) $args['cascade'] as $cascade) {
6 ✔
656
                        if (!isset($defaults[$cascade])) {
6 ✔
657
                                throw new InvalidModifierDefinitionException();
×
658
                        }
659
                        $property->relationship->cascade[$cascade] = true;
6 ✔
660
                }
661
                unset($args['cascade']);
6 ✔
662
        }
6 ✔
663

664

665
        /**
666
         * @param array<int|string, mixed> $args
667
         */
668
        protected function processRelationshipOrder(PropertyMetadata $property, array &$args): void
669
        {
670
                assert($property->relationship !== null);
671
                if (!isset($args['orderBy'])) {
6 ✔
672
                        return;
6 ✔
673
                }
674

675
                if (is_string($args['orderBy'])) {
6 ✔
676
                        $order = [$args['orderBy'] => ICollection::ASC];
6 ✔
677
                } elseif (is_array($args['orderBy']) && isset($args['orderBy'][0])) {
6 ✔
UNCOV
678
                        $order = [$args['orderBy'][0] => $args['orderBy'][1] ?? ICollection::ASC];
×
679
                } else {
680
                        $order = $args['orderBy'];
6 ✔
681
                }
682

683
                $property->relationship->order = $order;
6 ✔
684
                unset($args['orderBy']);
6 ✔
685
        }
6 ✔
686

687

688
        /**
689
         * @param array<int|string, mixed> $args
690
         */
691
        protected function processRelationshipExposeCollection(PropertyMetadata $property, array &$args): void
692
        {
693
                if (isset($args['exposeCollection']) && $args['exposeCollection'] === true) {
6 ✔
694
                        $property->args[HasMany::class]['exposeCollection'] = true;
6 ✔
695
                }
696
                unset($args['exposeCollection']);
6 ✔
697
        }
6 ✔
698

699

700
        /**
701
         * @param array<int|string, mixed> $args
702
         */
703
        protected function processRelationshipIsMain(PropertyMetadata $property, array &$args): void
704
        {
705
                assert($property->relationship !== null);
706
                $property->relationship->isMain = isset($args['isMain']) && $args['isMain'] === true;
6 ✔
707
                unset($args['isMain']);
6 ✔
708
        }
6 ✔
709
}
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