• 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

90.67
/src/Repository/Repository.php
1
<?php declare(strict_types = 1);
2

3
/**
4
 * This file is part of the Nextras\Orm library.
5
 * This file was inspired by PetrP's ORM library https://github.com/PetrP/Orm/.
6
 * @link       https://github.com/nextras/orm
7
 */
8

9
namespace Nextras\Orm\Repository;
10

11

12
use Nextras\Orm\Collection\ArrayCollection;
13
use Nextras\Orm\Collection\Functions\AvgAggregateFunction;
14
use Nextras\Orm\Collection\Functions\CollectionFunction;
15
use Nextras\Orm\Collection\Functions\CompareEqualsFunction;
16
use Nextras\Orm\Collection\Functions\CompareGreaterThanEqualsFunction;
17
use Nextras\Orm\Collection\Functions\CompareGreaterThanFunction;
18
use Nextras\Orm\Collection\Functions\CompareLikeFunction;
19
use Nextras\Orm\Collection\Functions\CompareNotEqualsFunction;
20
use Nextras\Orm\Collection\Functions\CompareSmallerThanEqualsFunction;
21
use Nextras\Orm\Collection\Functions\CompareSmallerThanFunction;
22
use Nextras\Orm\Collection\Functions\ConjunctionOperatorFunction;
23
use Nextras\Orm\Collection\Functions\CountAggregateFunction;
24
use Nextras\Orm\Collection\Functions\DisjunctionOperatorFunction;
25
use Nextras\Orm\Collection\Functions\FetchPropertyFunction;
26
use Nextras\Orm\Collection\Functions\MaxAggregateFunction;
27
use Nextras\Orm\Collection\Functions\MinAggregateFunction;
28
use Nextras\Orm\Collection\Functions\SumAggregateFunction;
29
use Nextras\Orm\Collection\Helpers\ConditionParser;
30
use Nextras\Orm\Collection\ICollection;
31
use Nextras\Orm\Entity\IEntity;
32
use Nextras\Orm\Entity\Reflection\EntityMetadata;
33
use Nextras\Orm\Exception\InvalidArgumentException;
34
use Nextras\Orm\Exception\InvalidStateException;
35
use Nextras\Orm\Exception\MemberAccessException;
36
use Nextras\Orm\Exception\NoResultException;
37
use Nextras\Orm\Exception\NotImplementedException;
38
use Nextras\Orm\Mapper\IMapper;
39
use Nextras\Orm\Model\IModel;
40
use Nextras\Orm\Model\MetadataStorage;
41
use ReflectionClass;
42
use function count;
43
use function sort;
44

45

46
/**
47
 * @template E of IEntity
48
 * @implements IRepository<E>
49
 */
50
abstract class Repository implements IRepository
51
{
52
        /** @var array<mixed, callable(E $entity): void> */
53
        public array $onBeforePersist = [];
54

55
        /** @var array<mixed, callable(E $entity): void> */
56
        public array $onAfterPersist = [];
57

58
        /** @var array<mixed, callable(E $entity): void> */
59
        public array $onBeforeInsert = [];
60

61
        /** @var array<mixed, callable(E $entity): void> */
62
        public array $onAfterInsert = [];
63

64
        /** @var array<mixed, callable(E $entity): void> */
65
        public array $onBeforeUpdate = [];
66

67
        /** @var array<mixed, callable(E $entity): void> */
68
        public array $onAfterUpdate = [];
69

70
        /** @var array<mixed, callable(E $entity): void> */
71
        public array $onBeforeRemove = [];
72

73
        /** @var array<mixed, callable(E $entity): void> */
74
        public array $onAfterRemove = [];
75

76
        /** @var array<mixed, callable(E[] $persisted, E[] $removed): void> */
77
        public array $onFlush = [];
78

79
        /** @var class-string<E>|null */
80
        protected string|null $entityClassName = null;
81

82
        private IModel|null $model = null;
83
        private ConditionParser|null $conditionParser = null;
84

85
        /** @var IdentityMap<E> */
86
        private IdentityMap $identityMap;
87

88
        /** @var array<string, bool> */
89
        private array $proxyMethods = [];
90

91
        /** @var array{list<E>, list<E>} */
92
        private array $entitiesToFlush = [[], []];
93

94
        /** @var array<string, CollectionFunction> Collection functions cache */
95
        private array $collectionFunctions = [];
96

97

98
        public function __construct(
5 ✔
99
                /** @var IMapper<E> */
100
                protected readonly IMapper $mapper,
1 ✔
101
                protected readonly IDependencyProvider|null $dependencyProvider = null,
1 ✔
102
        )
103
        {
104
                $this->mapper->setRepository($this);
6 ✔
105

106
                /** @var IdentityMap<E> $identityMap */
107
                $identityMap = new IdentityMap($this);
6 ✔
108
                $this->identityMap = $identityMap;
6 ✔
109

110
                $reflection = new ReflectionClass($this);
6 ✔
111
                preg_match_all(
6 ✔
112
                        '~^[ \t*]* @method[ \t]+[^\s]+[ \t]+(\w+)\(.*\).*$~um',
6 ✔
113
                        (string) $reflection->getDocComment(),
6 ✔
114
                        $matches,
115
                        PREG_SET_ORDER,
6 ✔
116
                );
117
                foreach ($matches as [, $methodname]) {
6 ✔
118
                        $this->proxyMethods[strtolower($methodname)] = true;
6 ✔
119
                }
120
        }
6 ✔
121

122

123
        #[\NoDiscard]
124
        public function getModel(): IModel
125
        {
126
                if ($this->model === null) {
6 ✔
127
                        throw new InvalidStateException('Repository is not attached to model.');
×
128
                }
129

130
                return $this->model;
6 ✔
131
        }
132

133

134
        public function setModel(IModel $model): void
135
        {
136
                if ($this->model !== null && $this->model !== $model) {
6 ✔
137
                        throw new InvalidStateException('Repository is already attached.');
×
138
                }
139

140
                $this->model = $model;
6 ✔
141
        }
6 ✔
142

143

144
        #[\NoDiscard]
145
        public function getMapper(): IMapper
146
        {
147
                return $this->mapper;
6 ✔
148
        }
149

150

151
        #[\NoDiscard]
152
        public function getBy(array $conds): IEntity|null
153
        {
154
                return $this->findAll()->getBy($conds);
6 ✔
155
        }
156

157

158
        #[\NoDiscard]
159
        public function getByChecked(array $conds): IEntity
160
        {
161
                $entity = $this->getBy($conds);
6 ✔
162
                if ($entity === null) {
6 ✔
163
                        throw new NoResultException();
×
164
                }
165
                return $entity;
6 ✔
166
        }
167

168

169
        #[\NoDiscard]
170
        public function getById($id): IEntity|null
171
        {
172
                if ($id === null) {
6 ✔
173
                        return null;
×
174
                }
175

176
                $entity = $this->identityMap->getById($id);
6 ✔
177
                if ($entity === false) { // entity was removed
6 ✔
178
                        return null;
×
179
                } elseif ($entity instanceof IEntity) {
6 ✔
180
                        return $entity;
6 ✔
181
                }
182

183
                $entity = $this->findAll()->getBy(['id' => $id]);
6 ✔
184
                if ($entity === null) {
6 ✔
185
                        $this->identityMap->remove($id);
6 ✔
186
                }
187

188
                return $entity;
6 ✔
189
        }
190

191

192
        #[\NoDiscard]
193
        public function getByIdChecked($id): IEntity
194
        {
195
                $entity = $this->getById($id);
6 ✔
196
                if ($entity === null) {
6 ✔
197
                        throw new NoResultException();
×
198
                }
199
                return $entity;
6 ✔
200
        }
201

202

203
        #[\NoDiscard]
204
        public function findAll(): ICollection
205
        {
206
                return $this->mapper->findAll();
6 ✔
207
        }
208

209

210
        #[\NoDiscard]
211
        public function findBy(array $conds): ICollection
212
        {
213
                return $this->findAll()->findBy($conds);
6 ✔
214
        }
215

216

217
        #[\NoDiscard]
218
        public function findByIds(array $ids): ICollection
219
        {
220
                $entities = [];
6 ✔
221
                $missingEntities = false;
6 ✔
222

223
                foreach ($ids as $id) {
6 ✔
224
                        $entity = $this->identityMap->getById($id);
6 ✔
225
                        if ($entity === null || $entity === false) {
6 ✔
226
                                $missingEntities = true;
6 ✔
227
                                break;
6 ✔
228
                        }
229
                        $entities[] = $entity;
6 ✔
230
                }
231

232
                if (!$missingEntities) {
6 ✔
233
                        return new ArrayCollection($entities, $this);
6 ✔
234
                }
235

236
                return $this->findAll()->findBy(['id' => $ids]);
6 ✔
237
        }
238

239

240
        #[\NoDiscard]
241
        public function getCollectionFunction(string $name): CollectionFunction
242
        {
243
                if (!isset($this->collectionFunctions[$name])) {
6 ✔
244
                        $this->collectionFunctions[$name] = $this->createCollectionFunction($name);
6 ✔
245
                }
246
                return $this->collectionFunctions[$name];
6 ✔
247
        }
248

249

250
        protected function createCollectionFunction(string $name): CollectionFunction
251
        {
252
                /** @var array<class-string<CollectionFunction>, true> $knownFunctions */
253
                static $knownFunctions = [
6 ✔
254
                        FetchPropertyFunction::class => true,
255
                        CompareEqualsFunction::class => true,
256
                        CompareGreaterThanEqualsFunction::class => true,
257
                        CompareGreaterThanFunction::class => true,
258
                        CompareNotEqualsFunction::class => true,
259
                        CompareSmallerThanEqualsFunction::class => true,
260
                        CompareSmallerThanFunction::class => true,
261
                        CompareLikeFunction::class => true,
262
                        AvgAggregateFunction::class => true,
263
                        CountAggregateFunction::class => true,
264
                        MaxAggregateFunction::class => true,
265
                        MinAggregateFunction::class => true,
266
                        SumAggregateFunction::class => true,
267
                ];
268

269
                if ($name === FetchPropertyFunction::class) {
6 ✔
270
                        return new FetchPropertyFunction($this, $this->mapper, $this->getModel());
6 ✔
271
                } elseif ($name === ConjunctionOperatorFunction::class) {
6 ✔
272
                        return new ConjunctionOperatorFunction($this->getConditionParser());
6 ✔
273
                } elseif ($name === DisjunctionOperatorFunction::class) {
6 ✔
274
                        return new DisjunctionOperatorFunction($this->getConditionParser());
6 ✔
275
                }
276

277
                if (isset($knownFunctions[$name])) {
6 ✔
278
                        /** @var CollectionFunction $function */
279
                        $function = new $name();
6 ✔
280
                        return $function;
6 ✔
281
                } else {
NEW
282
                        throw new NotImplementedException('Override ' . static::class . '::createCollectionFunction() to return an instance of ' . $name . ' collection function.');
×
283
                }
284
        }
285

286

287
        public function attach(IEntity $entity): void
288
        {
289
                if (!$entity->isAttached()) {
6 ✔
290
                        $entity->onAttach($this, MetadataStorage::get($entity::class));
6 ✔
291
                        if ($this->dependencyProvider !== null) {
6 ✔
292
                                $this->dependencyProvider->injectDependencies($entity);
6 ✔
293
                        }
294
                }
295
        }
6 ✔
296

297

298
        public function detach(IEntity $entity): void
299
        {
300
                if ($entity->isAttached()) {
6 ✔
301
                        $entity->onDetach();
6 ✔
302
                }
303
        }
6 ✔
304

305

306
        public function hydrateEntity(array $data): IEntity|null
307
        {
308
                return $this->identityMap->create($data);
6 ✔
309
        }
310

311

312
        #[\NoDiscard]
313
        public function getEntityMetadata(string|null $entityClass = null): EntityMetadata
314
        {
315
                $classNames = static::getEntityClassNames();
6 ✔
316
                if ($entityClass !== null && !in_array($entityClass, $classNames, true)) {
6 ✔
NEW
317
                        throw new InvalidArgumentException("Class '$entityClass' is not accepted by '" . static::class . "' repository.");
×
318
                }
319
                return MetadataStorage::get($entityClass ?? $classNames[0]);
6 ✔
320
        }
321

322

323
        #[\NoDiscard]
324
        public function getEntityClassName(array $data): string
325
        {
326
                if ($this->entityClassName === null) {
6 ✔
327
                        /** @var class-string<E> $entityClassName */
328
                        $entityClassName = static::getEntityClassNames()[0];
6 ✔
329
                        $this->entityClassName = $entityClassName;
6 ✔
330
                }
331

332
                return $this->entityClassName;
6 ✔
333
        }
334

335

336
        public function getConditionParser(): ConditionParser
337
        {
338
                if ($this->conditionParser === null) {
6 ✔
339
                        $this->conditionParser = new ConditionParser();
6 ✔
340
                }
341
                return $this->conditionParser;
6 ✔
342
        }
343

344

345
        public function persist(IEntity $entity, bool $withCascade = true): IEntity
346
        {
347
                $this->identityMap->check($entity);
6 ✔
348
                $this->getModel()->persist($entity, $withCascade);
6 ✔
349
                return $entity;
6 ✔
350
        }
351

352

353
        public function doPersist(IEntity $entity): void
354
        {
355
                if (!$entity->isModified()) {
6 ✔
356
                        return;
6 ✔
357
                }
358

359
                $isPersisted = $entity->isPersisted();
6 ✔
360
                if ($isPersisted) {
6 ✔
361
                        $this->onBeforeUpdate($entity);
6 ✔
362
                        $this->identityMap->remove($entity->getPersistedId()); // id can change in composite key
6 ✔
363
                } else {
364
                        $this->onBeforeInsert($entity);
6 ✔
365
                }
366

367
                $this->mapper->persist($entity);
6 ✔
368
                $this->identityMap->add($entity);
6 ✔
369
                $this->entitiesToFlush[0][] = $entity;
6 ✔
370

371
                if ($isPersisted) {
6 ✔
372
                        $this->onAfterUpdate($entity);
6 ✔
373
                } else {
374
                        $this->onAfterInsert($entity);
6 ✔
375
                }
376
                $this->onAfterPersist($entity);
6 ✔
377
        }
6 ✔
378

379

380
        public function remove(IEntity $entity, bool $withCascade = true): IEntity
381
        {
382
                $this->identityMap->check($entity);
6 ✔
383
                return $this->getModel()->remove($entity, $withCascade);
6 ✔
384
        }
385

386

387
        public function doRemove(IEntity $entity): void
388
        {
389
                $this->detach($entity);
6 ✔
390
                if (!$entity->isPersisted()) {
6 ✔
391
                        return;
×
392
                }
393

394
                $this->mapper->remove($entity);
6 ✔
395
                $this->identityMap->remove($entity->getPersistedId());
6 ✔
396
                $this->entitiesToFlush[1][] = $entity;
6 ✔
397
                $this->onAfterRemove($entity);
6 ✔
398
        }
6 ✔
399

400

401
        public function flush(): void
402
        {
403
                $this->getModel()->flush();
6 ✔
404
        }
6 ✔
405

406

407
        public function persistAndFlush(IEntity $entity, bool $withCascade = true): IEntity
408
        {
409
                $this->persist($entity, $withCascade);
6 ✔
410
                $this->flush();
6 ✔
411
                return $entity;
6 ✔
412
        }
413

414

415
        public function removeAndFlush(IEntity $entity, bool $withCascade = true): IEntity
416
        {
417
                $this->remove($entity, $withCascade);
6 ✔
418
                $this->flush();
6 ✔
419
                return $entity;
6 ✔
420
        }
421

422

423
        public function doFlush(): array
424
        {
425
                $this->mapper->flush();
6 ✔
426
                $this->onFlush($this->entitiesToFlush[0], $this->entitiesToFlush[1]);
6 ✔
427
                $entities = $this->entitiesToFlush;
6 ✔
428
                $this->entitiesToFlush = [[], []];
6 ✔
429
                return $entities;
6 ✔
430
        }
431

432

433
        public function doClear(): void
434
        {
435
                $this->identityMap->destroyAllEntities();
6 ✔
436
                $this->mapper->clearCache();
6 ✔
437
        }
6 ✔
438

439

440
        /**
441
         * @param mixed[] $args
442
         * @return mixed
443
         */
444
        public function __call(string $method, array $args)
445
        {
446
                if (isset($this->proxyMethods[strtolower($method)])) {
6 ✔
447
                        $callback = [$this->mapper, $method];
6 ✔
448
                        assert(is_callable($callback));
449
                        return call_user_func_array($callback, $args);
6 ✔
450
                } else {
NEW
451
                        $class = static::class;
×
452
                        throw new MemberAccessException("Undefined $class::$method() (proxy)method.");
×
453
                }
454
        }
455

456

457
        public function doRefreshAll(bool $allowOverwrite): void
458
        {
459
                $ids = [];
6 ✔
460
                $entities = $this->identityMap->getAll();
6 ✔
461
                foreach ($entities as $entity) {
6 ✔
462
                        if (!$entity->isPersisted()) {
6 ✔
463
                                continue;
×
464
                        } elseif (!$allowOverwrite && $entity->isModified()) {
6 ✔
465
                                throw new InvalidStateException('Cannot refresh modified entity, flush changes first or set $allowOverwrite flag to true.');
6 ✔
466
                        }
467
                        $this->identityMap->markForRefresh($entity);
6 ✔
468
                        $ids[] = $entity->getPersistedId();
6 ✔
469
                }
470
                if (count($ids) > 0) {
6 ✔
471
                        sort($ids); // make ids sorted deterministically
6 ✔
472
                        // the fetch itself refreshes the entities in the identity map
473
                        $_ = $this->findByIds($ids)->fetchAll();
6 ✔
474
                }
475
                foreach ($entities as $entity) {
6 ✔
476
                        if (!$this->identityMap->isMarkedForRefresh($entity)) {
6 ✔
477
                                continue;
6 ✔
478
                        }
479
                        $this->detach($entity);
6 ✔
480
                        $this->identityMap->remove($entity->getPersistedId());
6 ✔
481
                        $this->onAfterRemove($entity);
6 ✔
482
                }
483
                $this->mapper->clearCache();
6 ✔
484
        }
6 ✔
485

486

487
        public function onBeforePersist(IEntity $entity): void
488
        {
489
                $entity->onBeforePersist();
6 ✔
490
                foreach ($this->onBeforePersist as $handler) {
6 ✔
491
                        call_user_func($handler, $entity);
6 ✔
492
                }
493
        }
6 ✔
494

495

496
        public function onAfterPersist(IEntity $entity): void
497
        {
498
                $entity->onAfterPersist();
6 ✔
499
                foreach ($this->onAfterPersist as $handler) {
6 ✔
500
                        call_user_func($handler, $entity);
×
501
                }
502
        }
6 ✔
503

504

505
        public function onBeforeInsert(IEntity $entity): void
506
        {
507
                $entity->onBeforeInsert();
6 ✔
508
                foreach ($this->onBeforeInsert as $handler) {
6 ✔
509
                        call_user_func($handler, $entity);
×
510
                }
511
        }
6 ✔
512

513

514
        public function onAfterInsert(IEntity $entity): void
515
        {
516
                $entity->onAfterInsert();
6 ✔
517
                foreach ($this->onAfterInsert as $handler) {
6 ✔
518
                        call_user_func($handler, $entity);
6 ✔
519
                }
520
        }
6 ✔
521

522

523
        public function onBeforeUpdate(IEntity $entity): void
524
        {
525
                $entity->onBeforeUpdate();
6 ✔
526
                foreach ($this->onBeforeUpdate as $handler) {
6 ✔
527
                        call_user_func($handler, $entity);
×
528
                }
529
        }
6 ✔
530

531

532
        public function onAfterUpdate(IEntity $entity): void
533
        {
534
                $entity->onAfterUpdate();
6 ✔
535
                foreach ($this->onAfterUpdate as $handler) {
6 ✔
536
                        call_user_func($handler, $entity);
×
537
                }
538
        }
6 ✔
539

540

541
        public function onBeforeRemove(IEntity $entity): void
542
        {
543
                $entity->onBeforeRemove();
6 ✔
544
                foreach ($this->onBeforeRemove as $handler) {
6 ✔
545
                        call_user_func($handler, $entity);
×
546
                }
547
        }
6 ✔
548

549

550
        public function onAfterRemove(IEntity $entity): void
551
        {
552
                $entity->onAfterRemove();
6 ✔
553
                foreach ($this->onAfterRemove as $handler) {
6 ✔
554
                        call_user_func($handler, $entity);
×
555
                }
556
        }
6 ✔
557

558

559
        public function onFlush(array $persistedEntities, array $removedEntities): void
560
        {
561
                foreach ($this->onFlush as $handler) {
6 ✔
562
                        call_user_func($handler, $persistedEntities, $removedEntities);
6 ✔
563
                }
564
        }
6 ✔
565
}
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