• 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.91
/src/Mapper/Memory/ArrayMapper.php
1
<?php declare(strict_types = 1);
2

3
namespace Nextras\Orm\Mapper\Memory;
4

5

6
use DateTimeImmutable;
7
use Nextras\Orm\Collection\ArrayCollection;
8
use Nextras\Orm\Collection\ICollection;
9
use Nextras\Orm\Entity\IEntity;
10
use Nextras\Orm\Entity\Reflection\PropertyMetadata;
11
use Nextras\Orm\Exception\InvalidStateException;
12
use Nextras\Orm\Exception\IOException;
13
use Nextras\Orm\Exception\LogicException;
14
use Nextras\Orm\Mapper\IMapper;
15
use Nextras\Orm\Mapper\Memory\Conventions\Conventions;
16
use Nextras\Orm\Mapper\Memory\Conventions\IConventions;
17
use Nextras\Orm\Repository\IRepository;
18
use function array_keys;
19
use function array_values;
20
use function assert;
21

22

23
/**
24
 * @template E of IEntity
25
 * @implements IMapper<E>
26
 */
27
abstract class ArrayMapper implements IMapper
28
{
29
        /** @var array<int|string, mixed>|null */
30
        protected array|null $data = null;
31

32
        /** @var array<string, array<int|string, mixed>> */
33
        protected array $relationshipData = [];
34

35
        protected IConventions|null $conventions = null;
36

37
        /** @var resource|null */
38
        protected static $lock;
39

40
        /** @var IRepository<E>|null */
41
        private IRepository|null $repository = null;
42

43

44
        public function setRepository(IRepository $repository): void
45
        {
46
                if ($this->repository !== null && $this->repository !== $repository) {
6 ✔
NEW
47
                        $name = static::class;
×
48
                        throw new InvalidStateException("Mapper '$name' is already attached to repository.");
×
49
                }
50

51
                $this->repository = $repository;
6 ✔
52
        }
6 ✔
53

54

55
        #[\NoDiscard]
56
        public function getRepository(): IRepository
57
        {
58
                if ($this->repository === null) {
6 ✔
NEW
59
                        $name = static::class;
×
60
                        throw new InvalidStateException("Mapper '$name' is not attached to repository.");
×
61
                }
62
                /** @var IRepository<E> */
63
                return $this->repository;
6 ✔
64
        }
65

66

67
        #[\NoDiscard]
68
        public function findAll(): ICollection
69
        {
70
                return new ArrayCollection($this->getData(), $this->getRepository());
6 ✔
71
        }
72

73

74
        /**
75
         * @param list<E> $data
76
         * @return ICollection<E>
77
         */
78
        #[\NoDiscard]
79
        public function toCollection(array $data): ICollection
80
        {
81
                return new ArrayCollection($data, $this->getRepository());
×
82
        }
83

84

85
        #[\NoDiscard]
86
        public function createCollectionManyHasOne(PropertyMetadata $metadata): ICollection
87
        {
88
                $collection = $this->findAll();
6 ✔
89
                $collection->setRelationshipMapper(new RelationshipMapperManyHasOne($metadata));
6 ✔
90
                return $collection;
6 ✔
91
        }
92

93

94
        #[\NoDiscard]
95
        public function createCollectionOneHasOne(PropertyMetadata $metadata): ICollection
96
        {
97
                assert($metadata->relationship !== null);
98
                $collection = $this->findAll();
6 ✔
99
                $collection->setRelationshipMapper(
6 ✔
100
                        $metadata->relationship->isMain
6 ✔
101
                                ? new RelationshipMapperManyHasOne($metadata)
6 ✔
102
                                : new RelationshipMapperOneHasOne($this, $metadata),
6 ✔
103
                );
104
                return $collection;
6 ✔
105
        }
106

107

108
        #[\NoDiscard]
109
        public function createCollectionManyHasMany(IMapper $sourceMapper, PropertyMetadata $metadata): ICollection
110
        {
111
                assert($sourceMapper instanceof self);
112
                $collection = $this->findAll();
6 ✔
113
                $collection->setRelationshipMapper(new RelationshipMapperManyHasMany($this, $sourceMapper, $metadata));
6 ✔
114
                return $collection;
6 ✔
115
        }
116

117

118
        #[\NoDiscard]
119
        public function createCollectionOneHasMany(PropertyMetadata $metadata): ICollection
120
        {
121
                $collection = $this->findAll();
6 ✔
122
                $collection->setRelationshipMapper(new RelationshipMapperOneHasMany($this, $metadata));
6 ✔
123
                return $collection;
6 ✔
124
        }
125

126

127
        public function clearCache(): void
128
        {
129
                $this->data = null;
6 ✔
130
        }
6 ✔
131

132

133
        /**
134
         * @return array<int|string, mixed>
135
         */
136
        public function &getRelationshipDataStorage(string $key): array
137
        {
138
                $value = &$this->relationshipData[$key];
6 ✔
139
                $value = (array) $value; // @phpstan-ignore-line
6 ✔
140
                return $value;
6 ✔
141
        }
142

143

144
        public function persist(IEntity $entity): void
145
        {
146
                $this->initializeData();
6 ✔
147
                assert($this->data !== null);
148

149
                $data = $this->entityToArray($entity);
6 ✔
150
                $data = $this->getConventions()->convertEntityToStorage($data);
6 ✔
151

152
                if ($entity->isPersisted()) {
6 ✔
153
                        $id = $entity->getPersistedId();
6 ✔
154
                        $primaryValue = $this->getIdHash($id);
6 ✔
155
                } elseif ($entity->hasValue('id')) {
6 ✔
156
                        $id = $entity->getValue('id');
6 ✔
157
                        $primaryValue = $this->getIdHash($id);
6 ✔
158
                        if (isset($this->data[$primaryValue])) {
6 ✔
159
                                throw new InvalidStateException("Unique constraint violation: entity with '$primaryValue' primary value already exists.");
6 ✔
160
                        }
161
                } else {
162
                        $ids = array_keys($this->data);
6 ✔
163
                        $id = count($ids) > 0 ? ((int) max($ids)) + 1 : 1;
6 ✔
164
                        $primaryValue = $this->getIdHash($id);
6 ✔
165
                        $storagePrimaryKey = $this->getConventions()->getStoragePrimaryKey();
6 ✔
166
                        $data[$storagePrimaryKey[0]] = $id;
6 ✔
167
                }
168

169
                $this->data[$primaryValue] = $data;
6 ✔
170
                $entity->onPersist($id);
6 ✔
171
        }
6 ✔
172

173

174
        public function remove(IEntity $entity): void
175
        {
176
                $this->initializeData();
6 ✔
177
                assert($this->data !== null);
178

179
                $id = $this->getIdHash($entity->getPersistedId());
6 ✔
180
                $this->data[$id] = null;
6 ✔
181
        }
6 ✔
182

183

184
        public function flush(): void
185
        {
186
                try {
187
                        $this->lock();
6 ✔
188
                        if ($this->data === null) {
6 ✔
189
                                return;
6 ✔
190
                        }
191
                        $this->saveData([$this->data, $this->relationshipData]);
6 ✔
192
                } finally {
6 ✔
193
                        $this->unlock();
6 ✔
194
                }
195
        }
6 ✔
196

197

198
        public function rollback(): void
199
        {
200
                $this->data = null;
×
201
        }
×
202

203

204
        public function getConventions(): IConventions
205
        {
206
                if ($this->conventions === null) {
6 ✔
207
                        $this->conventions = $this->createConventions();
6 ✔
208
                }
209

210
                return $this->conventions;
6 ✔
211
        }
212

213

214
        protected function createConventions(): IConventions
215
        {
216
                return new Conventions($this->getRepository()->getEntityMetadata()->getPrimaryKey());
6 ✔
217
        }
218

219

220
        protected function initializeData(): void
221
        {
222
                if ($this->data !== null) {
6 ✔
223
                        return;
6 ✔
224
                }
225

226
                $stored = $this->readData();
6 ✔
227
                $this->data = $stored[0] ?? [];
6 ✔
228
                $this->relationshipData = $stored[1] ?? [];
6 ✔
229
        }
6 ✔
230

231

232
        /**
233
         * @return list<E>
234
         */
235
        protected function getData(): array
236
        {
237
                $this->initializeData();
6 ✔
238
                assert($this->data !== null);
239

240
                $repository = $this->getRepository();
6 ✔
241
                $conventions = $this->getConventions();
6 ✔
242

243
                $entities = [];
6 ✔
244
                foreach ($this->data as $row) {
6 ✔
245
                        if ($row === null) continue;
6 ✔
246
                        $entity = $repository->hydrateEntity($conventions->convertStorageToEntity($row));
6 ✔
247
                        if ($entity !== null) { // entity may have been deleted
6 ✔
248
                                $idHash = $this->getIdHash($entity->getPersistedId());
6 ✔
249
                                $entities[$idHash] = $entity;
6 ✔
250
                        }
251
                }
252

253
                return array_values($entities);
6 ✔
254
        }
255

256

257
        protected function lock(): void
258
        {
259
                if (self::$lock !== null) {
6 ✔
260
                        throw new LogicException('Critical section has already beed entered.');
×
261
                }
262

263
                $file = realpath(sys_get_temp_dir()) . '/NextrasOrmArrayMapper.lock.' . md5(__FILE__);
6 ✔
264
                $handle = fopen($file, 'c+');
6 ✔
265
                if ($handle === false) {
6 ✔
266
                        throw new IOException('Unable to create critical section.');
×
267
                }
268

269
                flock($handle, LOCK_EX);
6 ✔
270
                self::$lock = $handle;
6 ✔
271
        }
6 ✔
272

273

274
        protected function unlock(): void
275
        {
276
                if (self::$lock === null) {
6 ✔
277
                        throw new LogicException('Critical section has not been initialized.');
×
278
                }
279

280
                flock(self::$lock, LOCK_UN);
6 ✔
281
                fclose(self::$lock);
6 ✔
282
                self::$lock = null;
6 ✔
283
        }
6 ✔
284

285

286
        /**
287
         * @return array<string, mixed>
288
         */
289
        protected function entityToArray(IEntity $entity): array
290
        {
291
                /** @noinspection PhpRedundantOptionalArgumentInspection */
292
                return $entity->getRawValues();
6 ✔
293
        }
294

295

296
        /**
297
         * @param mixed $id
298
         */
299
        protected function getIdHash($id): string
300
        {
301
                if (!is_array($id)) {
6 ✔
302
                        return $id instanceof DateTimeImmutable
6 ✔
303
                                ? $id->format('c.u')
6 ✔
304
                                : (string) $id;
6 ✔
305
                }
306

307
                return implode(
6 ✔
308
                        ',',
4 ✔
309
                        array_map(
6 ✔
310
                                fn($id): string => $id instanceof DateTimeImmutable
6 ✔
311
                                        ? $id->format('c.u')
6 ✔
312
                                        : (string) $id,
6 ✔
313
                                $id,
314
                        ),
315
                );
316
        }
317

318

319
        /**
320
         * Reads stored data
321
         * @return array{
322
         *      0?: array<int|string, mixed>,
323
         *      1?: array<string, array<int|string, mixed>>
324
         * }
325
         */
326
        abstract protected function readData(): array;
327

328

329
        /**
330
         * Stores data
331
         * @param array{
332
         *      0?: array<int|string, mixed>,
333
         *      1?: array<string, array<int|string, mixed>>
334
         * } $data
335
         */
336
        abstract protected function saveData(array $data): void;
337
}
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