• 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

92.93
/src/Entity/AbstractEntity.php
1
<?php declare(strict_types = 1);
2

3
namespace Nextras\Orm\Entity;
4

5

6
use Nextras\Orm\Entity\Embeddable\EmbeddableContainer;
7
use Nextras\Orm\Entity\Reflection\EntityMetadata;
8
use Nextras\Orm\Entity\Reflection\PropertyMetadata;
9
use Nextras\Orm\Exception\InvalidArgumentException;
10
use Nextras\Orm\Exception\InvalidStateException;
11
use Nextras\Orm\Exception\LogicException;
12
use Nextras\Orm\Exception\NullValueException;
13
use Nextras\Orm\Relationships\IRelationshipCollection;
14
use Nextras\Orm\Relationships\IRelationshipContainer;
15
use Nextras\Orm\Repository\IRepository;
16
use function assert;
17

18

19
abstract class AbstractEntity implements IEntity
20
{
21
        use ImmutableDataTrait;
22

23
        private const EVERYTHING_MODIFIED_KEY = '';
24

25
        /** @var IRepository<IEntity>|null */
26
        private IRepository|null $repository = null;
27

28
        /** @var array<string, bool> */
29
        private array $modified = [];
30

31
        /** @var mixed */
32
        private $persistedId;
33

34

35
        public function __construct()
36
        {
37
                $this->modified[self::EVERYTHING_MODIFIED_KEY] = true;
6 ✔
38
                $this->metadata = $this->createMetadata();
6 ✔
39
                $this->onCreate();
6 ✔
40
        }
6 ✔
41

42

43
        #[\NoDiscard]
44
        public function getRepository(): IRepository
45
        {
46
                if ($this->repository === null) {
6 ✔
47
                        $entityClass = static::class;
6 ✔
48
                        throw new InvalidStateException("Entity '$entityClass' is not attached to its repository. Use IEntity::isAttached() method to check the state.");
6 ✔
49
                }
50
                return $this->repository;
6 ✔
51
        }
52

53

54
        #[\NoDiscard]
55
        public function isAttached(): bool
56
        {
57
                return $this->repository !== null;
6 ✔
58
        }
59

60

61
        #[\NoDiscard]
62
        public function getMetadata(): EntityMetadata
63
        {
64
                return $this->metadata;
6 ✔
65
        }
66

67

68
        #[\NoDiscard]
69
        public function isModified(string|null $name = null): bool
70
        {
71
                if ($name === null) {
6 ✔
72
                        return (bool) $this->modified;
6 ✔
73
                }
74

75
                $this->metadata->getProperty($name); // checks property existence
6 ✔
76
                return isset($this->modified[self::EVERYTHING_MODIFIED_KEY]) || isset($this->modified[$name]);
6 ✔
77
        }
78

79

80
        public function setAsModified(string|null $name = null): void
81
        {
82
                $this->modified[$name ?? self::EVERYTHING_MODIFIED_KEY] = true;
6 ✔
83
        }
6 ✔
84

85

86
        #[\NoDiscard]
87
        public function isPersisted(): bool
88
        {
89
                return $this->persistedId !== null;
6 ✔
90
        }
91

92

93
        #[\NoDiscard]
94
        public function getPersistedId()
95
        {
96
                return $this->persistedId;
6 ✔
97
        }
98

99

100
        public function setValue(string $name, $value)
101
        {
102
                $metadata = $this->metadata->getProperty($name);
6 ✔
103
                if ($metadata->isReadonly) {
6 ✔
104
                        throw new InvalidArgumentException("Property '$name' is read-only.");
×
105
                }
106

107
                $this->internalSetValue($metadata, $name, $value);
6 ✔
108
                return $this;
6 ✔
109
        }
110

111

112
        public function setReadOnlyValue(string $name, $value)
113
        {
114
                $metadata = $this->metadata->getProperty($name);
6 ✔
115
                $this->internalSetValue($metadata, $name, $value);
6 ✔
116
                return $this;
6 ✔
117
        }
118

119

120
        public function setRawValue(string $name, $value): void
121
        {
122
                $property = $this->metadata->getProperty($name);
6 ✔
123

124
                if (!isset($this->validated[$name])) {
6 ✔
125
                        $this->initProperty($property, $name, initValue: false);
6 ✔
126
                }
127

128
                if ($property->wrapper !== null) {
6 ✔
129
                        if ($this->data[$name] instanceof IProperty) {
6 ✔
130
                                $this->data[$name]->setRawValue($value);
6 ✔
131
                                return;
6 ✔
132
                        }
133
                } elseif ($property->isVirtual) {
6 ✔
134
                        $this->internalSetValue($property, $name, $value);
×
135
                        return;
×
136
                }
137

138
                $this->data[$name] = $value;
6 ✔
139
                $this->modified[$name] = true;
6 ✔
140
                $this->validated[$name] = false;
6 ✔
141
        }
6 ✔
142

143

144
        #[\NoDiscard]
145
        public function &getRawValue(string $name)
146
        {
147
                $property = $this->metadata->getProperty($name);
6 ✔
148

149
                if (!isset($this->validated[$name])) {
6 ✔
150
                        $this->initProperty($property, $name);
6 ✔
151
                }
152

153
                $value = $this->data[$name];
6 ✔
154

155
                if ($value instanceof IProperty) {
6 ✔
156
                        $value = $value->getRawValue();
6 ✔
157
                        return $value;
6 ✔
158
                }
159

160
                if ($property->isVirtual) {
6 ✔
161
                        $value = $this->internalGetValue($property, $name);
×
162
                        return $value;
×
163
                }
164

165
                return $value;
6 ✔
166
        }
167

168

169
        #[\NoDiscard]
170
        public function getProperty(string $name): IProperty
171
        {
172
                $propertyMetadata = $this->metadata->getProperty($name);
6 ✔
173
                if ($propertyMetadata->wrapper === null) {
6 ✔
NEW
174
                        $class = static::class;
×
175
                        throw new InvalidStateException("Property $class::\$$name does not have a property wrapper.");
×
176
                }
177
                if (!isset($this->validated[$name])) {
6 ✔
178
                        $this->initProperty($propertyMetadata, $name);
6 ✔
179
                }
180

181
                return $this->data[$name];
6 ✔
182
        }
183

184

185
        #[\NoDiscard]
186
        public function getRawProperty(string $name)
187
        {
188
                $propertyMetadata = $this->metadata->getProperty($name);
6 ✔
189
                if ($propertyMetadata->wrapper === null) {
6 ✔
NEW
190
                        $class = static::class;
×
191
                        throw new InvalidStateException("Property $class::\$$name does not have a property wrapper.");
×
192
                }
193
                return $this->data[$name] ?? null;
6 ✔
194
        }
195

196

197
        #[\NoDiscard]
198
        public function getRawValues(bool $modifiedOnly = false): array
199
        {
200
                $out = [];
6 ✔
201
                $exportModified = $modifiedOnly && $this->isPersisted();
6 ✔
202

203
                foreach ($this->metadata->getProperties() as $name => $propertyMetadata) {
6 ✔
204
                        if ($propertyMetadata->isVirtual) continue;
6 ✔
205
                        if ($propertyMetadata->isPrimary && !$this->hasValue($name)) continue;
6 ✔
206
                        if ($exportModified && !$this->isModified($name)) continue;
6 ✔
207

208
                        if ($propertyMetadata->wrapper === null) {
6 ✔
209
                                if (!isset($this->validated[$name])) {
6 ✔
210
                                        $this->initProperty($propertyMetadata, $name);
6 ✔
211
                                }
212
                                $out[$name] = $this->data[$name];
6 ✔
213
                        } else {
214
                                $out[$name] = $this->getProperty($name)->getRawValue();
6 ✔
215
                                if ($out[$name] === null && !$propertyMetadata->isNullable) {
6 ✔
216
                                        throw new NullValueException($propertyMetadata);
6 ✔
217
                                }
218
                        }
219
                }
220

221
                return $out;
6 ✔
222
        }
223

224

225
        /**
226
         * @param list<string> $skipProperties list of property names to be skipped, only top-entity properties can be skipped.
227
         * @return array<string, mixed>
228
         */
229
        public function toArray(int $mode = ToArrayConverter::RELATIONSHIP_AS_IS, array $skipProperties = []): array
230
        {
231
                return ToArrayConverter::toArray($this, $mode, skipProperties: $skipProperties);
6 ✔
232
        }
233

234

235
        public function __clone()
236
        {
237
                $id = $this->hasValue('id') ? $this->getValue('id') : null;
6 ✔
238
                $persistedId = $this->persistedId;
6 ✔
239
                $isAttached = $this->isAttached();
6 ✔
240
                foreach ($this->getMetadata()->getProperties() as $name => $metadataProperty) {
6 ✔
241
                        // getValue loads data & checks for not null values
242
                        if ($this->hasValue($name) && is_object($this->data[$name])) {
6 ✔
243
                                if ($this->data[$name] instanceof IRelationshipCollection) {
6 ✔
244
                                        $data = iterator_to_array($this->data[$name]->toCollection());
6 ✔
245
                                        $this->data['id'] = null;
6 ✔
246
                                        $this->persistedId = null;
6 ✔
247
                                        $this->data[$name] = clone $this->data[$name];
6 ✔
248
                                        assert($this->data[$name] instanceof IRelationshipCollection);
249
                                        $this->data[$name]->onEntityAttach($this);
6 ✔
250
                                        if ($isAttached) {
6 ✔
251
                                                $this->data[$name]->onEntityRepositoryAttach($this);
6 ✔
252
                                        }
253
                                        $this->data[$name]->set($data);
6 ✔
254
                                        $this->data['id'] = $id;
6 ✔
255
                                        $this->persistedId = $persistedId;
6 ✔
256
                                } elseif ($this->data[$name] instanceof IRelationshipContainer) {
6 ✔
257
                                        $this->data[$name] = clone $this->data[$name];
6 ✔
258
                                        $this->data[$name]->onEntityAttach($this);
6 ✔
259
                                        if ($isAttached) {
6 ✔
260
                                                $this->data[$name]->onEntityRepositoryAttach($this);
6 ✔
261
                                        }
262
                                } elseif ($this->data[$name] instanceof EmbeddableContainer) {
6 ✔
263
                                        $this->data[$name] = clone $this->data[$name];
6 ✔
264
                                        $this->data[$name]->onEntityAttach($this);
6 ✔
265
                                        if ($isAttached) {
6 ✔
266
                                                $this->data[$name]->onEntityRepositoryAttach($this);
6 ✔
267
                                        }
268
                                } else {
269
                                        $this->data[$name] = clone $this->data[$name];
6 ✔
270
                                }
271
                        }
272
                }
273
                $this->data['id'] = null;
6 ✔
274
                $this->persistedId = null;
6 ✔
275
                $this->modified[self::EVERYTHING_MODIFIED_KEY] = true;
6 ✔
276

277
                if ($this->repository !== null) {
6 ✔
278
                        $repository = $this->repository;
6 ✔
279
                        $this->repository = null;
6 ✔
280
                        $repository->attach($this);
6 ✔
281
                }
282
        }
6 ✔
283

284

285
        // === events ======================================================================================================
286

287
        public function onCreate(): void
288
        {
289
        }
6 ✔
290

291

292
        public function onLoad(array $data): void
293
        {
294
                foreach ($this->metadata->getProperties() as $name => $metadataProperty) {
6 ✔
295
                        if ($metadataProperty->isVirtual) continue;
6 ✔
296

297
                        if (isset($data[$name])) {
6 ✔
298
                                $this->data[$name] = $data[$name];
6 ✔
299
                        }
300
                }
301

302
                $this->persistedId = $this->getValue('id');
6 ✔
303
        }
6 ✔
304

305

306
        public function onRefresh(array|null $data, bool $isPartial = false): void
307
        {
308
                if ($data === null) {
6 ✔
309
                        throw new InvalidStateException('Refetching data failed. Entity is not present in storage anymore.');
6 ✔
310
                }
311
                if ($isPartial) {
6 ✔
312
                        foreach ($data as $name => $value) {
6 ✔
313
                                $this->data[$name] = $value;
6 ✔
314
                                unset($this->modified[$name], $this->validated[$name]);
6 ✔
315
                        }
316
                } else {
317
                        $this->data = $data;
6 ✔
318
                        $this->validated = [];
6 ✔
319
                        $this->modified = [];
6 ✔
320
                }
321
        }
6 ✔
322

323

324
        public function onFree(): void
325
        {
326
                $this->data = [];
6 ✔
327
                $this->persistedId = null;
6 ✔
328
                $this->validated = [];
6 ✔
329
        }
6 ✔
330

331

332
        public function onAttach(IRepository $repository, EntityMetadata $metadata): void
333
        {
334
                if ($this->isAttached()) {
6 ✔
335
                        return;
×
336
                }
337

338
                $this->repository = $repository;
6 ✔
339
                $this->metadata = $metadata;
6 ✔
340

341
                foreach ($this->data as $property) {
6 ✔
342
                        if ($property instanceof IEntityAwareProperty) {
6 ✔
343
                                $property->onEntityRepositoryAttach($this);
6 ✔
344
                        }
345
                }
346
        }
6 ✔
347

348

349
        public function onDetach(): void
350
        {
351
                $this->repository = null;
6 ✔
352
        }
6 ✔
353

354

355
        public function onPersist($id): void
356
        {
357
                // $id property may be marked as read-only @phpstan-ignore-next-line
358
                $this->setReadOnlyValue('id', $id);
6 ✔
359
                $this->persistedId = $this->getValue('id');
6 ✔
360
                $this->modified = [];
6 ✔
361
        }
6 ✔
362

363

364
        public function onBeforePersist(): void
365
        {
366
        }
6 ✔
367

368

369
        public function onAfterPersist(): void
370
        {
371
        }
6 ✔
372

373

374
        public function onBeforeInsert(): void
375
        {
376
        }
6 ✔
377

378

379
        public function onAfterInsert(): void
380
        {
381
        }
6 ✔
382

383

384
        public function onBeforeUpdate(): void
385
        {
386
        }
6 ✔
387

388

389
        public function onAfterUpdate(): void
390
        {
391
        }
6 ✔
392

393

394
        public function onBeforeRemove(): void
395
        {
396
        }
6 ✔
397

398

399
        public function onAfterRemove(): void
400
        {
401
                $this->repository = null;
6 ✔
402
                $this->persistedId = null;
6 ✔
403
                $this->modified = [];
6 ✔
404
        }
6 ✔
405

406

407
        // === internal implementation =====================================================================================
408

409

410
        /**
411
         * @param mixed $value
412
         */
413
        private function internalSetValue(PropertyMetadata $metadata, string $name, $value): void
414
        {
415
                if (!isset($this->validated[$name])) {
6 ✔
416
                        $this->initProperty($metadata, $name, initValue: false);
6 ✔
417
                }
418

419
                $property = $this->data[$name];
6 ✔
420
                if ($property instanceof IPropertyContainer) {
6 ✔
421
                        if ($property->setInjectedValue($value)) {
6 ✔
422
                                $this->setAsModified($name);
6 ✔
423
                        }
424
                        return;
6 ✔
425
                } elseif ($property instanceof IProperty) {
6 ✔
NEW
426
                        $class = static::class;
×
427
                        throw new LogicException("You cannot set property wrapper's value in $class::\$$name directly.");
×
428
                }
429

430
                if ($metadata->hasSetter !== null) {
6 ✔
431
                        /** @var callable $cb */
432
                        $cb = [$this, $metadata->hasSetter];
6 ✔
433
                        $value = call_user_func($cb, $value, $metadata);
6 ✔
434
                        if ($metadata->isVirtual) {
6 ✔
435
                                $this->modified[$name] = true;
×
436
                                return;
×
437
                        }
438
                }
439

440
                $this->validate($metadata, $name, $value);
6 ✔
441
                $this->data[$name] = $value;
6 ✔
442
                $this->modified[$name] = true;
6 ✔
443
        }
6 ✔
444

445

446
        protected function initProperty(PropertyMetadata $metadata, string $name, bool $initValue = true): void
447
        {
448
                $this->validated[$name] = true;
6 ✔
449

450
                if (!isset($this->data[$name]) && !array_key_exists($name, $this->data)) {
6 ✔
451
                        $this->data[$name] = $this->persistedId === null ? $metadata->defaultValue : null;
6 ✔
452
                }
453

454
                if ($metadata->wrapper !== null) {
6 ✔
455
                        $wrapper = $this->createPropertyWrapper($metadata);
6 ✔
456
                        if ($initValue || isset($this->data[$metadata->name])) {
6 ✔
457
                                $wrapper->setRawValue($this->data[$metadata->name] ?? null);
6 ✔
458
                        }
459
                        $this->data[$name] = $wrapper;
6 ✔
460
                        return;
6 ✔
461
                }
462

463

464
                if ($this->data[$name] !== null) {
6 ✔
465
                        // data type coercion
466
                        // we validate only when value is not a null to not validate the missing value
467
                        // from db or which has not been set yet
468
                        $this->validate($metadata, $name, $this->data[$name]);
6 ✔
469
                }
470
        }
6 ✔
471

472

473
        private function createPropertyWrapper(PropertyMetadata $metadata): IProperty
474
        {
475
                $class = $metadata->wrapper;
6 ✔
476
                assert($class !== null);
477
                $wrapper = new $class($metadata);
6 ✔
478
                assert($wrapper instanceof IProperty);
479

480
                if ($wrapper instanceof IEntityAwareProperty) {
6 ✔
481
                        $wrapper->onEntityAttach($this);
6 ✔
482
                        if ($this->isAttached()) {
6 ✔
483
                                $wrapper->onEntityRepositoryAttach($this);
6 ✔
484
                        }
485
                }
486

487
                return $wrapper;
6 ✔
488
        }
489
}
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