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

nextras / orm / 27441222947

12 Jun 2026 08:29PM UTC coverage: 91.915% (-0.2%) from 92.162%
27441222947

Pull #810

github

web-flow
Merge 191dbb3fe into 7a20304be
Pull Request #810: Fix DbalCollection::getQueryBuilder()

61 of 66 new or added lines in 3 files covered. (92.42%)

6 existing lines in 3 files now uncovered.

4263 of 4638 relevant lines covered (91.91%)

1.81 hits per line

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

90.69
/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;
2✔
101
                $this->modifierParser = new ModifierParser();
2✔
102

103
                // phpdoc-parser 2.0
104
                if (class_exists('PHPStan\PhpDocParser\ParserConfig')) {
2✔
105
                        $config = new ParserConfig(usedAttributes: []); // @phpstan-ignore-line
2✔
106
                        $this->phpDocLexer = new Lexer($config); // @phpstan-ignore-line
2✔
107
                        $constExprParser = new ConstExprParser($config); // @phpstan-ignore-line
2✔
108
                        $typeParser = new TypeParser($config, $constExprParser); // @phpstan-ignore-line
2✔
109
                        $this->phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); // @phpstan-ignore-line
2✔
110
                } else {
UNCOV
111
                        $this->phpDocLexer = new Lexer(); // @phpstan-ignore-line
×
UNCOV
112
                        $constExprParser = new ConstExprParser(); // @phpstan-ignore-line
×
UNCOV
113
                        $typeParser = new TypeParser($constExprParser); // @phpstan-ignore-line
×
UNCOV
114
                        $this->phpDocParser = new PhpDocParser($typeParser, $constExprParser); // @phpstan-ignore-line
×
115
                }
116
        }
2✔
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);
2✔
133
                $this->metadata = new EntityMetadata($entityClass);
2✔
134

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

138
                if ($fileDependencies !== null) {
2✔
139
                        $fileDependencies = array_values(array_unique($fileDependencies));
2✔
140
                }
141
                return $this->metadata;
2✔
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];
2✔
151
                while (($current = get_parent_class($current)) !== false) {
2✔
152
                        $classTree[] = $current;
2✔
153
                }
154

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

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

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

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

186
                        foreach ($this->classPropertiesCache[$class] as $name => $property) {
2✔
187
                                $this->metadata->setProperty($name, $property);
2✔
188
                        }
189
                }
190
        }
2✔
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();
2✔
201
                if ($docComment === false) return [];
2✔
202

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

206
                $properties = [];
2✔
207
                foreach ($phpDocNode->getPropertyTagValues() as $propertyTagValue) {
2✔
208
                        $property = $this->parseProperty($propertyTagValue, $reflection->getName(), $methods, isReadonly: false);
2✔
209
                        $properties[$property->name] = $property;
2✔
210
                }
211
                foreach ($phpDocNode->getPropertyWriteTagValues() as $propertyTagValue) {
2✔
212
                        $property = $this->parseProperty($propertyTagValue, $reflection->getName(), $methods, isReadonly: false);
×
213
                        $properties[$property->name] = $property;
×
214
                }
215
                foreach ($phpDocNode->getPropertyReadTagValues() as $propertyTagValue) {
2✔
216
                        $property = $this->parseProperty($propertyTagValue, $reflection->getName(), $methods, isReadonly: true);
2✔
217
                        $properties[$property->name] = $property;
2✔
218
                }
219
                return $properties;
2✔
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();
2✔
234
                $property->name = substr($propertyNode->propertyName, 1);
2✔
235
                $property->containerClassname = $containerClassName;
2✔
236
                $property->isReadonly = $isReadonly;
2✔
237

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

245

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

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

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

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

280
                                if ($expandedSubTypeLower === 'null') {
2✔
281
                                        $property->isNullable = true;
2✔
282
                                        continue;
2✔
283
                                }
284
                                if ($expandedSubType === DateTime::class || is_subclass_of($expandedSubType, DateTime::class)) {
2✔
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])) {
2✔
288
                                        /** @var string $expandedSubType */
289
                                        $expandedSubType = $aliases[$expandedSubTypeLower];
×
290
                                }
291
                                $parsedTypes[$expandedSubType] = true;
2✔
292
                        } elseif ($subType instanceof ArrayTypeNode) {
2✔
293
                                $parsedTypes['array'] = true;
2✔
294
                        } elseif ($subType instanceof ArrayShapeNode) {
2✔
295
                                $parsedTypes['array'] = true;
2✔
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) {
2✔
304
                        throw new NotSupportedException("Property {$this->currentReflection->name}::\${$property->name} without a type definition is not supported.");
×
305
                }
306
                $property->types = $parsedTypes;
2✔
307
        }
2✔
308

309

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

316
                $matches = $this->modifierParser->matchModifiers($propertyComment);
2✔
317
                foreach ($matches as $macroContent) {
2✔
318
                        try {
319
                                $args = $this->modifierParser->parse($macroContent, $this->currentReflection);
2✔
320
                        } catch (InvalidModifierDefinitionException $e) {
2✔
321
                                throw new InvalidModifierDefinitionException(
2✔
322
                                        "Invalid modifier definition for {$this->currentReflection->name}::\${$property->name} property.",
2✔
323
                                        0,
2✔
324
                                        $e,
325
                                );
326
                        }
327
                        $this->processPropertyModifier($property, $args[0], $args[1]);
2✔
328
                }
329
        }
2✔
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);
2✔
338
                if (isset($methods[$getter])) {
2✔
339
                        $property->hasGetter = $getter;
2✔
340
                }
341
                $setter = 'setter' . strtolower($property->name);
2✔
342
                if (isset($methods[$setter])) {
2✔
343
                        $property->hasSetter = $setter;
×
344
                }
345
        }
2✔
346

347

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

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

362

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

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

396

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

412

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

428

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

441

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

458

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

468

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

474

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

484

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

501

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

510

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

516

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

526

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

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

545

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

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

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

565
                $this->metadata->setPrimaryKey($primaryKey);
2✔
566
        }
2✔
567

568

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

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

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

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

608
                /** @var class-string<IEntity> $entity */
609
                $entity = Reflection::expandClassName($class, $this->currentReflection);
2✔
610
                if (!isset($this->entityClassesMap[$entity])) {
2✔
611
                        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.");
2✔
612
                }
613

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

619

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

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

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

646

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

657
                if (is_string($args['orderBy'])) {
2✔
658
                        $order = [$args['orderBy'] => ICollection::ASC];
2✔
659

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

663
                } else {
664
                        $order = $args['orderBy'];
2✔
665
                }
666

667
                $property->relationship->order = $order;
2✔
668
                unset($args['orderBy']);
2✔
669
        }
2✔
670

671

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

683

684
        /**
685
         * @param array<int|string, mixed> $args
686
         */
687
        protected function processRelationshipIsMain(PropertyMetadata $property, array &$args): void
688
        {
689
                assert($property->relationship !== null);
690
                $property->relationship->isMain = isset($args['isMain']) && $args['isMain'] === true;
2✔
691
                unset($args['isMain']);
2✔
692
        }
2✔
693
}
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

© 2026 Coveralls, Inc