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

nextras / orm / 13350186865

16 Feb 2025 12:54AM UTC coverage: 92.021% (-0.03%) from 92.052%
13350186865

push

github

web-flow
Merge pull request #731 from nextras/primary-proxy-through-wrapper

Model PrimaryProxy modifier as property wrapper

35 of 35 new or added lines in 2 files covered. (100.0%)

4 existing lines in 1 file now uncovered.

4071 of 4424 relevant lines covered (92.02%)

4.59 hits per line

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

92.31
/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
use function get_class;
18

19

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

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 = null;
33

34

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

42

43
        public function getRepository(): IRepository
44
        {
45
                if ($this->repository === null) {
5✔
46
                        throw new InvalidStateException('Entity is not attached to a repository. Use IEntity::isAttached() method to check the state.');
5✔
47
                }
48
                return $this->repository;
5✔
49
        }
50

51

52
        public function isAttached(): bool
53
        {
54
                return $this->repository !== null;
5✔
55
        }
56

57

58
        public function getMetadata(): EntityMetadata
59
        {
60
                return $this->metadata;
5✔
61
        }
62

63

64
        public function isModified(string|null $name = null): bool
65
        {
66
                if ($name === null) {
5✔
67
                        return (bool) $this->modified;
5✔
68
                }
69

70
                $this->metadata->getProperty($name); // checks property existence
5✔
71
                return isset($this->modified[null]) || isset($this->modified[$name]);
5✔
72
        }
73

74

75
        public function setAsModified(string|null $name = null): void
76
        {
77
                $this->modified[$name] = true;
5✔
78
        }
5✔
79

80

81
        public function isPersisted(): bool
82
        {
83
                return $this->persistedId !== null;
5✔
84
        }
85

86

87
        public function getPersistedId()
88
        {
89
                return $this->persistedId;
5✔
90
        }
91

92

93
        public function setValue(string $name, $value)
94
        {
95
                $metadata = $this->metadata->getProperty($name);
5✔
96
                if ($metadata->isReadonly) {
5✔
97
                        throw new InvalidArgumentException("Property '$name' is read-only.");
×
98
                }
99

100
                $this->internalSetValue($metadata, $name, $value);
5✔
101
                return $this;
5✔
102
        }
103

104

105
        public function setReadOnlyValue(string $name, $value)
106
        {
107
                $metadata = $this->metadata->getProperty($name);
5✔
108
                $this->internalSetValue($metadata, $name, $value);
5✔
109
                return $this;
5✔
110
        }
111

112

113
        public function setRawValue(string $name, $value): void
114
        {
115
                $property = $this->metadata->getProperty($name);
5✔
116

117
                if ($property->wrapper !== null) {
5✔
118
                        if ($this->data[$name] instanceof IProperty) {
5✔
119
                                $this->data[$name]->setRawValue($value);
5✔
120
                                return;
5✔
121
                        }
122
                } elseif ($property->isVirtual) {
5✔
123
                        $this->internalSetValue($property, $name, $value);
×
124
                        return;
×
125
                }
126

127
                $this->data[$name] = $value;
5✔
128
                $this->modified[$name] = true;
5✔
129
                $this->validated[$name] = false;
5✔
130
        }
5✔
131

132

133
        public function &getRawValue(string $name)
134
        {
135
                $property = $this->metadata->getProperty($name);
5✔
136

137
                if (!isset($this->validated[$name])) {
5✔
138
                        $this->initProperty($property, $name);
5✔
139
                }
140

141
                $value = $this->data[$name];
5✔
142

143
                if ($value instanceof IProperty) {
5✔
144
                        $value = $value->getRawValue();
5✔
145
                        return $value;
5✔
146
                }
147

148
                if ($property->isVirtual) {
5✔
UNCOV
149
                        $value = $this->internalGetValue($property, $name);
×
UNCOV
150
                        return $value;
×
151
                }
152

153
                return $value;
5✔
154
        }
155

156

157
        public function getProperty(string $name): IProperty
158
        {
159
                $propertyMetadata = $this->metadata->getProperty($name);
5✔
160
                if ($propertyMetadata->wrapper === null) {
5✔
161
                        $class = get_class($this);
×
162
                        throw new InvalidStateException("Property $class::\$$name does not have a property wrapper.");
×
163
                }
164
                if (!isset($this->validated[$name])) {
5✔
165
                        $this->initProperty($propertyMetadata, $name);
5✔
166
                }
167

168
                return $this->data[$name];
5✔
169
        }
170

171

172
        public function getRawProperty(string $name)
173
        {
174
                $propertyMetadata = $this->metadata->getProperty($name);
5✔
175
                if ($propertyMetadata->wrapper === null) {
5✔
176
                        $class = get_class($this);
×
177
                        throw new InvalidStateException("Property $class::\$$name does not have a property wrapper.");
×
178
                }
179
                return $this->data[$name] ?? null;
5✔
180
        }
181

182

183
        public function getRawValues(bool $modifiedOnly = false): array
184
        {
185
                $out = [];
5✔
186
                $exportModified = $modifiedOnly && $this->isPersisted();
5✔
187

188
                foreach ($this->metadata->getProperties() as $name => $propertyMetadata) {
5✔
189
                        if ($propertyMetadata->isVirtual) continue;
5✔
190
                        if ($propertyMetadata->isPrimary && !$this->hasValue($name)) continue;
5✔
191
                        if ($exportModified && !$this->isModified($name)) continue;
5✔
192

193
                        if ($propertyMetadata->wrapper === null) {
5✔
194
                                if (!isset($this->validated[$name])) {
5✔
195
                                        $this->initProperty($propertyMetadata, $name);
5✔
196
                                }
197
                                $out[$name] = $this->data[$name];
5✔
198

199
                        } else {
200
                                $out[$name] = $this->getProperty($name)->getRawValue();
5✔
201
                                if ($out[$name] === null && !$propertyMetadata->isNullable) {
5✔
202
                                        throw new NullValueException($propertyMetadata);
5✔
203
                                }
204
                        }
205
                }
206

207
                return $out;
5✔
208
        }
209

210

211
        /**
212
         * @return array<string, mixed>
213
         */
214
        public function toArray(int $mode = ToArrayConverter::RELATIONSHIP_AS_IS): array
215
        {
216
                return ToArrayConverter::toArray($this, $mode);
×
217
        }
218

219

220
        public function __clone()
221
        {
222
                $id = $this->hasValue('id') ? $this->getValue('id') : null;
5✔
223
                $persistedId = $this->persistedId;
5✔
224
                $isAttached = $this->isAttached();
5✔
225
                foreach ($this->getMetadata()->getProperties() as $name => $metadataProperty) {
5✔
226
                        // getValue loads data & checks for not null values
227
                        if ($this->hasValue($name) && is_object($this->data[$name])) {
5✔
228
                                if ($this->data[$name] instanceof IRelationshipCollection) {
5✔
229
                                        $data = iterator_to_array($this->data[$name]->toCollection());
5✔
230
                                        $this->data['id'] = null;
5✔
231
                                        $this->persistedId = null;
5✔
232
                                        $this->data[$name] = clone $this->data[$name];
5✔
233
                                        $this->data[$name]->onEntityAttach($this);
5✔
234
                                        if ($isAttached) {
5✔
235
                                                $this->data[$name]->onEntityRepositoryAttach($this);
5✔
236
                                        }
237
                                        $this->data[$name]->set($data);
5✔
238
                                        $this->data['id'] = $id;
5✔
239
                                        $this->persistedId = $persistedId;
5✔
240

241
                                } elseif ($this->data[$name] instanceof IRelationshipContainer) {
5✔
242
                                        $this->data[$name] = clone $this->data[$name];
5✔
243
                                        $this->data[$name]->onEntityAttach($this);
5✔
244
                                        if ($isAttached) {
5✔
245
                                                $this->data[$name]->onEntityRepositoryAttach($this);
5✔
246
                                        }
247

248
                                } elseif ($this->data[$name] instanceof EmbeddableContainer) {
5✔
249
                                        $this->data[$name] = clone $this->data[$name];
5✔
250
                                        $this->data[$name]->onEntityAttach($this);
5✔
251
                                        if ($isAttached) {
5✔
252
                                                $this->data[$name]->onEntityRepositoryAttach($this);
5✔
253
                                        }
254

255
                                } else {
256
                                        $this->data[$name] = clone $this->data[$name];
5✔
257
                                }
258
                        }
259
                }
260
                $this->data['id'] = null;
5✔
261
                $this->persistedId = null;
5✔
262
                $this->modified[null] = true;
5✔
263

264
                if ($this->repository !== null) {
5✔
265
                        $repository = $this->repository;
5✔
266
                        $this->repository = null;
5✔
267
                        $repository->attach($this);
5✔
268
                }
269
        }
5✔
270

271

272
        // === events ======================================================================================================
273

274
        public function onCreate(): void
275
        {
276
        }
5✔
277

278

279
        public function onLoad(array $data): void
280
        {
281
                foreach ($this->metadata->getProperties() as $name => $metadataProperty) {
5✔
282
                        if ($metadataProperty->isVirtual) continue;
5✔
283

284
                        if (isset($data[$name])) {
5✔
285
                                $this->data[$name] = $data[$name];
5✔
286
                        }
287
                }
288

289
                $this->persistedId = $this->getValue('id');
5✔
290
        }
5✔
291

292

293
        public function onRefresh(?array $data, bool $isPartial = false): void
294
        {
295
                if ($data === null) {
5✔
296
                        throw new InvalidStateException('Refetching data failed. Entity is not present in storage anymore.');
5✔
297
                }
298
                if ($isPartial) {
5✔
299
                        foreach ($data as $name => $value) {
5✔
300
                                $this->data[$name] = $value;
5✔
301
                                unset($this->modified[$name], $this->validated[$name]);
5✔
302
                        }
303

304
                } else {
305
                        $this->data = $data;
5✔
306
                        $this->validated = [];
5✔
307
                        $this->modified = [];
5✔
308
                }
309
        }
5✔
310

311

312
        public function onFree(): void
313
        {
314
                $this->data = [];
5✔
315
                $this->persistedId = null;
5✔
316
                $this->validated = [];
5✔
317
        }
5✔
318

319

320
        public function onAttach(IRepository $repository, EntityMetadata $metadata): void
321
        {
322
                if ($this->isAttached()) {
5✔
323
                        return;
×
324
                }
325

326
                $this->repository = $repository;
5✔
327
                $this->metadata = $metadata;
5✔
328

329
                foreach ($this->data as $property) {
5✔
330
                        if ($property instanceof IEntityAwareProperty) {
5✔
331
                                $property->onEntityRepositoryAttach($this);
5✔
332
                        }
333
                }
334
        }
5✔
335

336

337
        public function onDetach(): void
338
        {
339
                $this->repository = null;
5✔
340
        }
5✔
341

342

343
        public function onPersist($id): void
344
        {
345
                // $id property may be marked as read-only @phpstan-ignore-next-line
346
                $this->setReadOnlyValue('id', $id);
5✔
347
                $this->persistedId = $this->getValue('id');
5✔
348
                $this->modified = [];
5✔
349
        }
5✔
350

351

352
        public function onBeforePersist(): void
353
        {
354
        }
5✔
355

356

357
        public function onAfterPersist(): void
358
        {
359
        }
5✔
360

361

362
        public function onBeforeInsert(): void
363
        {
364
        }
5✔
365

366

367
        public function onAfterInsert(): void
368
        {
369
        }
5✔
370

371

372
        public function onBeforeUpdate(): void
373
        {
374
        }
5✔
375

376

377
        public function onAfterUpdate(): void
378
        {
379
        }
5✔
380

381

382
        public function onBeforeRemove(): void
383
        {
384
        }
5✔
385

386

387
        public function onAfterRemove(): void
388
        {
389
                $this->repository = null;
5✔
390
                $this->persistedId = null;
5✔
391
                $this->modified = [];
5✔
392
        }
5✔
393

394

395
        // === internal implementation =====================================================================================
396

397

398
        /**
399
         * @param mixed $value
400
         */
401
        private function internalSetValue(PropertyMetadata $metadata, string $name, $value): void
402
        {
403
                if (!isset($this->validated[$name])) {
5✔
404
                        $this->initProperty($metadata, $name, /* $initValue = */ false);
5✔
405
                }
406

407
                $property = $this->data[$name];
5✔
408
                if ($property instanceof IPropertyContainer) {
5✔
409
                        if ($property->setInjectedValue($value)) {
5✔
410
                                $this->setAsModified($name);
5✔
411
                        }
412
                        return;
5✔
413
                } elseif ($property instanceof IProperty) {
5✔
414
                        $class = get_class($this);
×
415
                        throw new LogicException("You cannot set property wrapper's value in $class::\$$name directly.");
×
416
                }
417

418
                if ($metadata->hasSetter !== null) {
5✔
419
                        /** @var callable $cb */
420
                        $cb = [$this, $metadata->hasSetter];
5✔
421
                        $value = call_user_func($cb, $value, $metadata);
5✔
422
                        if ($metadata->isVirtual) {
5✔
UNCOV
423
                                $this->modified[$name] = true;
×
UNCOV
424
                                return;
×
425
                        }
426
                }
427

428
                $this->validate($metadata, $name, $value);
5✔
429
                $this->data[$name] = $value;
5✔
430
                $this->modified[$name] = true;
5✔
431
        }
5✔
432

433

434
        protected function initProperty(PropertyMetadata $metadata, string $name, bool $initValue = true): void
435
        {
436
                $this->validated[$name] = true;
5✔
437

438
                if (!isset($this->data[$name]) && !array_key_exists($name, $this->data)) {
5✔
439
                        $this->data[$name] = $this->persistedId === null ? $metadata->defaultValue : null;
5✔
440
                }
441

442
                if ($metadata->wrapper !== null) {
5✔
443
                        $wrapper = $this->createPropertyWrapper($metadata);
5✔
444
                        if ($initValue || isset($this->data[$metadata->name])) {
5✔
445
                                $wrapper->setRawValue($this->data[$metadata->name] ?? null);
5✔
446
                        }
447
                        $this->data[$name] = $wrapper;
5✔
448
                        return;
5✔
449
                }
450

451

452
                if ($this->data[$name] !== null) {
5✔
453
                        // data type coercion
454
                        // we validate only when value is not a null to not validate the missing value
455
                        // from db or which has not been set yet
456
                        $this->validate($metadata, $name, $this->data[$name]);
5✔
457
                }
458
        }
5✔
459

460

461
        private function createPropertyWrapper(PropertyMetadata $metadata): IProperty
462
        {
463
                $class = $metadata->wrapper;
5✔
464
                $wrapper = new $class($metadata);
5✔
465
                assert($wrapper instanceof IProperty);
466

467
                if ($wrapper instanceof IEntityAwareProperty) {
5✔
468
                        $wrapper->onEntityAttach($this);
5✔
469
                        if ($this->isAttached()) {
5✔
470
                                $wrapper->onEntityRepositoryAttach($this);
5✔
471
                        }
472
                }
473

474
                return $wrapper;
5✔
475
        }
476
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc