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

valkyrjaio / valkyrja / 16040879868

03 Jul 2025 03:29AM UTC coverage: 40.245% (+0.3%) from 39.939%
16040879868

push

github

MelechMizrachi
Cache: Simplifying component.

9 of 64 new or added lines in 4 files covered. (14.06%)

6 existing lines in 2 files now uncovered.

4033 of 10021 relevant lines covered (40.25%)

5.06 hits per line

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

0.0
/src/Valkyrja/Orm/Repository/CacheRepository.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <melechmizrachi@gmail.com>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13

14
namespace Valkyrja\Orm\Repository;
15

16
use JsonException;
17
use Throwable;
18
use Valkyrja\Cache\Contract\Cache;
19
use Valkyrja\Exception\InvalidArgumentException;
20
use Valkyrja\Exception\RuntimeException;
21
use Valkyrja\Orm\Contract\Orm;
22
use Valkyrja\Orm\Driver\Contract\Driver;
23
use Valkyrja\Orm\Entity\Contract\Entity;
24
use Valkyrja\Orm\Exception\EntityNotFoundException;
25
use Valkyrja\Orm\Persister\Contract\Persister;
26
use Valkyrja\Orm\QueryBuilder\Contract\QueryBuilder;
27
use Valkyrja\Orm\Repository\Contract\CacheRepository as Contract;
28
use Valkyrja\Orm\Repository\Enum\StoreType;
29
use Valkyrja\Type\BuiltIn\Support\Arr;
30
use Valkyrja\Type\BuiltIn\Support\Obj;
31

32
use function base64_decode;
33
use function is_array;
34
use function is_int;
35
use function is_string;
36
use function md5;
37
use function method_exists;
38
use function serialize;
39
use function spl_object_id;
40
use function unserialize;
41

42
/**
43
 * Class CacheRepository.
44
 *
45
 * @author Melech Mizrachi
46
 *
47
 * @template Entity of Entity
48
 *
49
 * @extends Repository<Entity>
50
 *
51
 * @implements Contract<Entity>
52
 */
53
class CacheRepository extends Repository implements Contract
54
{
55
    /**
56
     * The id of a findOne (to tag if null returned).
57
     *
58
     * @var int|string|null
59
     */
60
    protected int|string|null $id;
61

62
    /**
63
     * The entities awaiting to be stored.
64
     *
65
     * @var Entity[]
66
     */
67
    protected array $storeEntities = [];
68

69
    /**
70
     * The entities awaiting to be forgotten.
71
     *
72
     * @var Entity[]
73
     */
74
    protected array $forgetEntities = [];
75

76
    /**
77
     * CacheRepository constructor.
78
     *
79
     * @param Orm                  $orm       The orm manager
80
     * @param Driver               $driver    The driver
81
     * @param Persister<Entity>    $persister The persister
82
     * @param Cache                $cache     The cache service
83
     * @param class-string<Entity> $entity    The entity class name
84
     */
85
    public function __construct(
86
        Orm $orm,
87
        Driver $driver,
88
        Persister $persister,
89
        protected Cache $cache,
90
        string $entity
91
    ) {
92
        parent::__construct(
×
UNCOV
93
            orm: $orm,
×
94
            driver: $driver,
×
95
            persister: $persister,
×
96
            entity: $entity
×
97
        );
×
98
    }
99

100
    /**
101
     * @inheritDoc
102
     */
103
    public function findOne(int|string $id): static
104
    {
105
        parent::findOne($id);
×
106

107
        $this->id = $id;
×
108

109
        return $this;
×
110
    }
111

112
    /**
113
     * @inheritDoc
114
     */
115
    public function where(
116
        string $column,
117
        string|null $operator = null,
118
        mixed $value = null,
119
        bool $setType = true
120
    ): static {
121
        if (! ($value instanceof QueryBuilder) && $column === $this->entity::getIdField()) {
×
122
            if (! is_string($value) && ! is_int($value)) {
×
123
                throw new InvalidArgumentException('ID should be either a string or int');
×
124
            }
125

126
            $this->id = $value;
×
127
        }
128

129
        parent::where($column, $operator, $value, $setType);
×
130

131
        return $this;
×
132
    }
133

134
    /**
135
     * @inheritDoc
136
     *
137
     * @throws JsonException
138
     */
139
    public function getResult(): array
140
    {
141
        $cacheKey = $this->getCacheKey();
×
142

NEW
143
        if (($results = $this->cache->get($cacheKey)) !== null && $results !== '') {
×
144
            try {
145
                $decodedResults = base64_decode($results, true);
×
146

147
                if ($decodedResults === false) {
×
148
                    throw new RuntimeException('Failed to decode results');
×
149
                }
150

151
                $results = unserialize($decodedResults, ['allowed_classes' => true]);
×
152

153
                if (! is_array($results)) {
×
154
                    throw new RuntimeException('Unserialized results were not an array');
×
155
                }
156

157
                if ($results === []) {
×
158
                    return [];
×
159
                }
160

161
                if (! $results[0] instanceof Entity) {
×
162
                    throw new RuntimeException('Unserialized results were not an array of entities');
×
163
                }
164

165
                if (method_exists($this, 'setRelationshipsOnEntities')) {
×
166
                    $this->setRelationshipsOnEntities(...$results);
×
167
                }
168

169
                /** @var Entity[] $results */
170
                return $results;
×
171
            } catch (Throwable) {
×
172
            }
173

174
            // Remove the bad cache
NEW
175
            $this->cache->forget($cacheKey);
×
176
        }
177

178
        $results = $this->retriever->getResult();
×
179

180
        $this->cacheResults($cacheKey, $results);
×
181

182
        if (method_exists($this, 'setRelationshipsOnEntities')) {
×
183
            $this->setRelationshipsOnEntities(...$results);
×
184
        }
185

186
        $this->id = null;
×
187

188
        return $results;
×
189
    }
190

191
    /**
192
     * @inheritDoc
193
     */
194
    public function getOneOrFail(): Entity
195
    {
196
        $results = $this->getOneOrNull();
×
197

198
        if ($results === null) {
×
199
            throw new EntityNotFoundException('Entity Not Found');
×
200
        }
201

202
        return $results;
×
203
    }
204

205
    /**
206
     * @inheritDoc
207
     *
208
     * @throws JsonException
209
     */
210
    public function getCount(): int
211
    {
212
        $cacheKey = $this->getCacheKey();
×
213

NEW
214
        if (($results = $this->cache->get($cacheKey)) !== null && $results !== '') {
×
215
            return (int) $results;
×
216
        }
217

218
        $results = parent::getCount();
×
219

NEW
220
        $this->cache->forever($cacheKey, (string) $results);
×
221

NEW
222
        $this->cache->getTagger($this->entity)->tag($cacheKey);
×
223

224
        return $results;
×
225
    }
226

227
    /**
228
     * @inheritDoc
229
     */
230
    public function create(Entity $entity, bool $defer = true): void
231
    {
232
        parent::create($entity, $defer);
×
233

234
        $this->deferOrCache(StoreType::store, $entity, $defer);
×
235
    }
236

237
    /**
238
     * @inheritDoc
239
     */
240
    public function save(Entity $entity, bool $defer = true): void
241
    {
242
        parent::save($entity, $defer);
×
243

244
        $this->deferOrCache(StoreType::store, $entity, $defer);
×
245
    }
246

247
    /**
248
     * @inheritDoc
249
     */
250
    public function delete(Entity $entity, bool $defer = true): void
251
    {
252
        parent::delete($entity, $defer);
×
253

254
        $this->deferOrCache(StoreType::forget, $entity, $defer);
×
255
    }
256

257
    /**
258
     * @inheritDoc
259
     */
260
    public function clear(Entity|null $entity = null): void
261
    {
262
        parent::clear($entity);
×
263

264
        if ($entity === null) {
×
265
            $this->clearDeferred();
×
266

267
            return;
×
268
        }
269

270
        // Get the id of the object
271
        $id = spl_object_id($entity);
×
272

273
        // If the model is set to be stored
274
        if (isset($this->storeEntities[$id])) {
×
275
            // Unset it
276
            unset($this->storeEntities[$id]);
×
277

278
            return;
×
279
        }
280

281
        // If the model is set to be forgotten
282
        if (isset($this->forgetEntities[$id])) {
×
283
            // Unset it
284
            unset($this->forgetEntities[$id]);
×
285
        }
286
    }
287

288
    /**
289
     * @inheritDoc
290
     */
291
    public function persist(): bool
292
    {
293
        $persist = parent::persist();
×
294

295
        $this->persistSave();
×
296
        $this->persistDelete();
×
297
        $this->clearDeferred();
×
298

299
        return $persist;
×
300
    }
301

302
    /**
303
     * Get cache key.
304
     *
305
     * @throws JsonException
306
     *
307
     * @return string
308
     */
309
    protected function getCacheKey(): string
310
    {
311
        return md5(
×
312
            Arr::toString(Obj::getAllProperties($this->retriever))
×
313
            . Arr::toString(Obj::getAllProperties($this->retriever->getQueryBuilder()))
×
314
        );
×
315
    }
316

317
    /**
318
     * Defer or cache.
319
     *
320
     * @param StoreType $type   Whether to store or forget
321
     * @param Entity    $entity The entity
322
     * @param bool      $defer  [optional] Whether to defer
323
     *
324
     * @return void
325
     */
326
    protected function deferOrCache(StoreType $type, Entity $entity, bool $defer = true): void
327
    {
328
        if ($defer) {
×
329
            $this->setDeferredEntity($type, $entity);
×
330

331
            return;
×
332
        }
333

334
        $this->forgetEntity($entity);
×
335
    }
336

337
    /**
338
     * Set a deferred entity.
339
     *
340
     * @param StoreType $type
341
     * @param Entity    $entity
342
     *
343
     * @return void
344
     */
345
    protected function setDeferredEntity(StoreType $type, Entity $entity): void
346
    {
347
        $id = spl_object_id($entity);
×
348

349
        match ($type) {
×
350
            StoreType::store  => $this->storeEntities[$id]  = $entity,
×
351
            StoreType::forget => $this->forgetEntities[$id] = $entity,
×
352
        };
×
353
    }
354

355
    /**
356
     * Forget entity in cache.
357
     *
358
     * @param Entity $entity
359
     *
360
     * @return void
361
     */
362
    protected function forgetEntity(Entity $entity): void
363
    {
364
        $id = $this->getEntityCacheKey($entity);
×
365

NEW
366
        $this->cache->getTagger($id)->flush();
×
367
    }
368

369
    /**
370
     * Get entity cache key.
371
     *
372
     * @param Entity $entity
373
     *
374
     * @return string
375
     */
376
    protected function getEntityCacheKey(Entity $entity): string
377
    {
378
        return $entity::class . ((string) $entity->getIdValue());
×
379
    }
380

381
    /**
382
     * Clear deferred entities.
383
     *
384
     * @return void
385
     */
386
    protected function clearDeferred(): void
387
    {
388
        $this->storeEntities  = [];
×
389
        $this->forgetEntities = [];
×
390
    }
391

392
    /**
393
     * Persist entities to be saved.
394
     *
395
     * @return void
396
     */
397
    protected function persistSave(): void
398
    {
399
        foreach ($this->storeEntities as $sid => $entity) {
×
400
            $this->forgetEntity($entity);
×
401

402
            unset($this->storeEntities[$sid]);
×
403
        }
404
    }
405

406
    /**
407
     * Persist entities to be deleted.
408
     *
409
     * @return void
410
     */
411
    protected function persistDelete(): void
412
    {
413
        foreach ($this->forgetEntities as $sid => $entity) {
×
414
            $this->forgetEntity($entity);
×
415

416
            unset($this->forgetEntities[$sid]);
×
417
        }
418
    }
419

420
    /**
421
     * Cache results.
422
     *
423
     * @param string                   $cacheKey
424
     * @param int|Entity|Entity[]|null $results
425
     *
426
     * @return void
427
     */
428
    protected function cacheResults(string $cacheKey, Entity|array|int|null $results): void
429
    {
430
        $id     = $this->id;
×
431
        $tags   = $id !== null && $id !== ''
×
432
            ? [(string) $this->id]
×
433
            : [];
×
434
        $tags[] = $this->entity;
×
435

436
        if (is_array($results)) {
×
437
            $tags = [];
×
438

439
            foreach ($results as $result) {
×
440
                $tags[] = $this->getEntityCacheKey($result);
×
441
            }
442
        }
443

NEW
444
        $this->cache->forever($cacheKey, base64_encode(serialize($results)));
×
445

NEW
446
        $this->cache->getTagger(...$tags)->tag($cacheKey);
×
447
    }
448
}
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

© 2026 Coveralls, Inc