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

nextras / orm / 18633402011

19 Oct 2025 04:58PM UTC coverage: 91.569% (-0.03%) from 91.597%
18633402011

push

github

web-flow
Weak Reference in Indentity map  (#764)

* better error message when Entity is detached

* implement WeakReference in IdentityMap

closes #739

* update output for MemoryManagement test (which does not run on CI)

25 of 27 new or added lines in 3 files covered. (92.59%)

2 existing lines in 2 files now uncovered.

4149 of 4531 relevant lines covered (91.57%)

4.57 hits per line

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

94.52
/src/Repository/IdentityMap.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
 * @license    MIT
7
 * @link       https://github.com/nextras/orm
8
 */
9

10
namespace Nextras\Orm\Repository;
11

12

13
use DateTimeImmutable;
14
use Nextras\Orm\Entity\IEntity;
15
use Nextras\Orm\Entity\IEntityHasPreloadContainer;
16
use Nextras\Orm\Exception\InvalidArgumentException;
17
use ReflectionClass;
18

19

20
/**
21
 * @template E of IEntity
22
 */
23
class IdentityMap
24
{
25
        /** @var array<int|string, \WeakReference<E>|false> */
26
        private array $entities = [];
27

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

31
        /** @var array<class-string<E>, ReflectionClass<E>> */
32
        private array $entityReflections = [];
33

34

35
        /**
36
         * @param IRepository<E> $repository
37
         */
38
        public function __construct(
5✔
39
                private readonly IRepository $repository,
40
        )
41
        {
42
        }
5✔
43

44

45
        /**
46
         * @param array|int|mixed $id
47
         */
48
        public function hasById($id): bool
49
        {
50
                $idHash = $this->getIdHash($id);
×
NEW
51
                return isset($this->entities[$idHash]) && ($this->entities[$idHash] === false || $this->entities[$idHash]->get() !== null);
×
52
        }
53

54

55
        /**
56
         * @param array|int|mixed $id
57
         * @return E|null|false
58
         */
59
        public function getById($id)
60
        {
61
                $idHash = $this->getIdHash($id);
5✔
62
                if (!isset($this->entities[$idHash]) || $this->entities[$idHash] === false || isset($this->entitiesForRefresh[$idHash])) {
5✔
63
                        return null;
5✔
64
                }
65

66
                $entity = $this->entities[$idHash]->get();
5✔
67
                if ($entity instanceof IEntityHasPreloadContainer) {
5✔
68
                        $entity->setPreloadContainer(null);
5✔
69
                }
70
                return $entity;
5✔
71
        }
72

73

74
        /**
75
         * @param E $entity
76
         */
77
        public function add(IEntity $entity): void
78
        {
79
                $id = $this->getIdHash($entity->getPersistedId());
5✔
80
                $this->entities[$id] = \WeakReference::create($entity);
5✔
81
        }
5✔
82

83

84
        /**
85
         * @param array|int|mixed $id
86
         */
87
        public function remove($id): void
88
        {
89
                $idHash = $this->getIdHash($id);
5✔
90
                $this->entities[$idHash] = false;
5✔
91
                unset($this->entitiesForRefresh[$idHash]);
5✔
92
        }
5✔
93

94

95
        /**
96
         * @param array<string, mixed> $data
97
         * @return E|null
98
         */
99
        public function create(array $data): ?IEntity
100
        {
101
                $entity = $this->createEntity($data);
5✔
102
                $id = $this->getIdHash($entity->getPersistedId());
5✔
103

104
                if (isset($this->entities[$id])) {
5✔
105
                        if ($this->entities[$id] === false) {
5✔
NEW
106
                                $this->repository->detach($entity);
×
UNCOV
107
                                return null;
×
108
                        }
109
                        $existingEntity = $this->entities[$id]->get();
5✔
110
                        if ($existingEntity === null) {
5✔
111
                                // The entity was garbage collected, use the new one
112
                                $this->entities[$id] = \WeakReference::create($entity);
5✔
113
                                return $entity;
5✔
114
                        }
115
                        $this->repository->detach($entity);
5✔
116
                        if (isset($this->entitiesForRefresh[$id])) {
5✔
117
                                // entity can be detached because of delete try
118
                                $this->repository->attach($existingEntity);
5✔
119
                                $existingEntity->onRefresh($data);
5✔
120
                                unset($this->entitiesForRefresh[$id]);
5✔
121
                        }
122
                        return $existingEntity;
5✔
123
                }
124

125
                $this->entities[$id] = \WeakReference::create($entity);
5✔
126
                return $entity;
5✔
127
        }
128

129

130
        /**
131
         * @return list<E>
132
         */
133
        public function getAll(): array
134
        {
135
                $all = [];
5✔
136
                foreach ($this->entities as $entity) {
5✔
137
                        if ($entity !== false && $entity->get() !== null) {
5✔
138
                                $all[] = $entity->get();
5✔
139
                        }
140
                }
141
                return $all;
5✔
142
        }
143

144

145
        public function check(IEntity $entity): void
146
        {
147
                if (!in_array(get_class($entity), ($this->repository)::getEntityClassNames(), true)) {
5✔
148
                        throw new InvalidArgumentException("Entity '" . get_class($entity) . "' is not accepted by '" . get_class($this->repository) . "' repository.");
5✔
149
                }
150
        }
5✔
151

152

153
        public function destroyAllEntities(): void
154
        {
155
                foreach ($this->entities as $entity) {
5✔
156
                        if ($entity !== false && $entity->get() !== null) {
5✔
157
                                $this->repository->detach($entity->get());
5✔
158
                                $entity->get()->onFree();
5✔
159
                        }
160
                }
161

162
                $this->entities = [];
5✔
163
        }
5✔
164

165

166
        public function markForRefresh(IEntity $entity): void
167
        {
168
                $id = $this->getIdHash($entity->getPersistedId());
5✔
169
                $this->entitiesForRefresh[$id] = true;
5✔
170
        }
5✔
171

172

173
        public function isMarkedForRefresh(IEntity $entity): bool
174
        {
175
                $id = $this->getIdHash($entity->getPersistedId());
5✔
176
                return isset($this->entitiesForRefresh[$id]);
5✔
177
        }
178

179

180
        /**
181
         * @param array<string, mixed> $data
182
         * @return E
183
         */
184
        protected function createEntity(array $data): IEntity
185
        {
186
                $entityClass = $this->repository->getEntityClassName($data);
5✔
187
                $this->entityReflections[$entityClass] ??= new ReflectionClass($entityClass);
5✔
188
                $entity = $this->entityReflections[$entityClass]->newInstanceWithoutConstructor();
5✔
189
                $this->repository->attach($entity);
5✔
190
                $entity->onLoad($data);
5✔
191
                return $entity;
5✔
192
        }
193

194

195
        /**
196
         * @param mixed $id
197
         */
198
        protected function getIdHash($id): string
199
        {
200
                if (!is_array($id)) {
5✔
201
                        return $id instanceof DateTimeImmutable
5✔
202
                                ? $id->format('c.u')
5✔
203
                                : (string) $id;
5✔
204
                }
205

206
                return implode(
5✔
207
                        ',',
4✔
208
                        array_map(
5✔
209
                                function ($id): string {
5✔
210
                                        return $id instanceof DateTimeImmutable
5✔
211
                                                ? $id->format('c.u')
5✔
212
                                                : (string) $id;
5✔
213
                                },
5✔
214
                                $id,
215
                        ),
216
                );
217
        }
218
}
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