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

valkyrjaio / valkyrja / 15659580871

15 Jun 2025 04:44AM UTC coverage: 47.202%. Remained the same
15659580871

push

github

MelechMizrachi
PHP CS Fixer.

37 of 75 new or added lines in 13 files covered. (49.33%)

2 existing lines in 2 files now uncovered.

5331 of 11294 relevant lines covered (47.2%)

16.12 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\Cache\Driver\Contract\Driver as CacheDriver;
20
use Valkyrja\Exception\InvalidArgumentException;
21
use Valkyrja\Exception\RuntimeException;
22
use Valkyrja\Orm\Contract\Orm;
23
use Valkyrja\Orm\Driver\Contract\Driver;
24
use Valkyrja\Orm\Entity\Contract\Entity;
25
use Valkyrja\Orm\Exception\EntityNotFoundException;
26
use Valkyrja\Orm\Persister\Contract\Persister;
27
use Valkyrja\Orm\QueryBuilder\Contract\QueryBuilder;
28
use Valkyrja\Orm\Repository\Contract\CacheRepository as Contract;
29
use Valkyrja\Orm\Repository\Enum\StoreType;
30
use Valkyrja\Type\BuiltIn\Support\Arr;
31
use Valkyrja\Type\BuiltIn\Support\Obj;
32

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

43
/**
44
 * Class CacheRepository.
45
 *
46
 * @author Melech Mizrachi
47
 *
48
 * @template Entity of Entity
49
 *
50
 * @extends Repository<Entity>
51
 *
52
 * @implements Contract<Entity>
53
 */
54
class CacheRepository extends Repository implements Contract
55
{
56
    /**
57
     * The cache store.
58
     *
59
     * @var CacheDriver
60
     */
61
    protected CacheDriver $store;
62

63
    /**
64
     * The id of a findOne (to tag if null returned).
65
     *
66
     * @var int|string|null
67
     */
68
    protected int|string|null $id;
69

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

77
    /**
78
     * The entities awaiting to be forgotten.
79
     *
80
     * @var Entity[]
81
     */
82
    protected array $forgetEntities = [];
83

84
    /**
85
     * CacheRepository constructor.
86
     *
87
     * @param Orm                  $orm       The orm manager
88
     * @param Driver               $driver    The driver
89
     * @param Persister<Entity>    $persister The persister
90
     * @param Cache                $cache     The cache service
91
     * @param class-string<Entity> $entity    The entity class name
92
     */
93
    public function __construct(
94
        Orm $orm,
95
        Driver $driver,
96
        Persister $persister,
97
        protected Cache $cache,
98
        string $entity
99
    ) {
100
        $this->store = $cache->use();
×
101

102
        parent::__construct(
×
103
            orm: $orm,
×
104
            driver: $driver,
×
105
            persister: $persister,
×
106
            entity: $entity
×
107
        );
×
108
    }
109

110
    /**
111
     * @inheritDoc
112
     */
113
    public function findOne(int|string $id): static
114
    {
115
        parent::findOne($id);
×
116

117
        $this->id = $id;
×
118

119
        return $this;
×
120
    }
121

122
    /**
123
     * @inheritDoc
124
     */
125
    public function where(
126
        string $column,
127
        string|null $operator = null,
128
        mixed $value = null,
129
        bool $setType = true
130
    ): static {
131
        if (! ($value instanceof QueryBuilder) && $column === $this->entity::getIdField()) {
×
132
            if (! is_string($value) && ! is_int($value)) {
×
133
                throw new InvalidArgumentException('ID should be either a string or int');
×
134
            }
135

136
            $this->id = $value;
×
137
        }
138

139
        parent::where($column, $operator, $value, $setType);
×
140

141
        return $this;
×
142
    }
143

144
    /**
145
     * @inheritDoc
146
     *
147
     * @throws JsonException
148
     */
149
    public function getResult(): array
150
    {
151
        $cacheKey = $this->getCacheKey();
×
152

153
        if (($results = $this->store->get($cacheKey)) !== null && $results !== '') {
×
154
            try {
155
                $decodedResults = base64_decode($results, true);
×
156

157
                if ($decodedResults === false) {
×
158
                    throw new RuntimeException('Failed to decode results');
×
159
                }
160

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

163
                if (! is_array($results)) {
×
164
                    throw new RuntimeException('Unserialized results were not an array');
×
165
                }
166

167
                if ($results === []) {
×
168
                    return [];
×
169
                }
170

171
                if (! $results[0] instanceof Entity) {
×
172
                    throw new RuntimeException('Unserialized results were not an array of entities');
×
173
                }
174

175
                if (method_exists($this, 'setRelationshipsOnEntities')) {
×
176
                    $this->setRelationshipsOnEntities(...$results);
×
177
                }
178

179
                /** @var Entity[] $results */
180
                return $results;
×
181
            } catch (Throwable) {
×
182
            }
183

184
            // Remove the bad cache
185
            $this->store->forget($cacheKey);
×
186
        }
187

188
        $results = $this->retriever->getResult();
×
189

190
        $this->cacheResults($cacheKey, $results);
×
191

192
        if (method_exists($this, 'setRelationshipsOnEntities')) {
×
193
            $this->setRelationshipsOnEntities(...$results);
×
194
        }
195

196
        $this->id = null;
×
197

198
        return $results;
×
199
    }
200

201
    /**
202
     * @inheritDoc
203
     */
204
    public function getOneOrFail(): Entity
205
    {
206
        $results = $this->getOneOrNull();
×
207

208
        if ($results === null) {
×
209
            throw new EntityNotFoundException('Entity Not Found');
×
210
        }
211

212
        return $results;
×
213
    }
214

215
    /**
216
     * @inheritDoc
217
     *
218
     * @throws JsonException
219
     */
220
    public function getCount(): int
221
    {
222
        $cacheKey = $this->getCacheKey();
×
223

224
        if (($results = $this->store->get($cacheKey)) !== null && $results !== '') {
×
225
            return (int) $results;
×
226
        }
227

228
        $results = parent::getCount();
×
229

230
        $this->store->forever($cacheKey, (string) $results);
×
231

232
        $this->store->getTagger($this->entity)->tag($cacheKey);
×
233

234
        return $results;
×
235
    }
236

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

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

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

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

257
    /**
258
     * @inheritDoc
259
     */
260
    public function delete(Entity $entity, bool $defer = true): void
261
    {
262
        parent::delete($entity, $defer);
×
263

264
        $this->deferOrCache(StoreType::forget, $entity, $defer);
×
265
    }
266

267
    /**
268
     * @inheritDoc
269
     */
270
    public function clear(Entity|null $entity = null): void
271
    {
272
        parent::clear($entity);
×
273

274
        if ($entity === null) {
×
275
            $this->clearDeferred();
×
276

277
            return;
×
278
        }
279

280
        // Get the id of the object
281
        $id = spl_object_id($entity);
×
282

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

288
            return;
×
289
        }
290

291
        // If the model is set to be forgotten
292
        if (isset($this->forgetEntities[$id])) {
×
293
            // Unset it
294
            unset($this->forgetEntities[$id]);
×
295
        }
296
    }
297

298
    /**
299
     * @inheritDoc
300
     */
301
    public function persist(): bool
302
    {
303
        $persist = parent::persist();
×
304

305
        $this->persistSave();
×
306
        $this->persistDelete();
×
307
        $this->clearDeferred();
×
308

309
        return $persist;
×
310
    }
311

312
    /**
313
     * Get cache key.
314
     *
315
     * @throws JsonException
316
     *
317
     * @return string
318
     */
319
    protected function getCacheKey(): string
320
    {
321
        return md5(
×
322
            Arr::toString(Obj::getAllProperties($this->retriever))
×
323
            . Arr::toString(Obj::getAllProperties($this->retriever->getQueryBuilder()))
×
324
        );
×
325
    }
326

327
    /**
328
     * Defer or cache.
329
     *
330
     * @param StoreType $type   Whether to store or forget
331
     * @param Entity    $entity The entity
332
     * @param bool      $defer  [optional] Whether to defer
333
     *
334
     * @return void
335
     */
336
    protected function deferOrCache(StoreType $type, Entity $entity, bool $defer = true): void
337
    {
338
        if ($defer) {
×
339
            $this->setDeferredEntity($type, $entity);
×
340

341
            return;
×
342
        }
343

344
        $this->forgetEntity($entity);
×
345
    }
346

347
    /**
348
     * Set a deferred entity.
349
     *
350
     * @param StoreType $type
351
     * @param Entity    $entity
352
     *
353
     * @return void
354
     */
355
    protected function setDeferredEntity(StoreType $type, Entity $entity): void
356
    {
357
        $id = spl_object_id($entity);
×
358

359
        match ($type) {
×
NEW
360
            StoreType::store  => $this->storeEntities[$id]  = $entity,
×
361
            StoreType::forget => $this->forgetEntities[$id] = $entity,
×
362
        };
×
363
    }
364

365
    /**
366
     * Forget entity in cache.
367
     *
368
     * @param Entity $entity
369
     *
370
     * @return void
371
     */
372
    protected function forgetEntity(Entity $entity): void
373
    {
374
        $id = $this->getEntityCacheKey($entity);
×
375

376
        $this->store->getTagger($id)->flush();
×
377
    }
378

379
    /**
380
     * Get entity cache key.
381
     *
382
     * @param Entity $entity
383
     *
384
     * @return string
385
     */
386
    protected function getEntityCacheKey(Entity $entity): string
387
    {
388
        return $entity::class . ((string) $entity->getIdValue());
×
389
    }
390

391
    /**
392
     * Clear deferred entities.
393
     *
394
     * @return void
395
     */
396
    protected function clearDeferred(): void
397
    {
398
        $this->storeEntities  = [];
×
399
        $this->forgetEntities = [];
×
400
    }
401

402
    /**
403
     * Persist entities to be saved.
404
     *
405
     * @return void
406
     */
407
    protected function persistSave(): void
408
    {
409
        foreach ($this->storeEntities as $sid => $entity) {
×
410
            $this->forgetEntity($entity);
×
411

412
            unset($this->storeEntities[$sid]);
×
413
        }
414
    }
415

416
    /**
417
     * Persist entities to be deleted.
418
     *
419
     * @return void
420
     */
421
    protected function persistDelete(): void
422
    {
423
        foreach ($this->forgetEntities as $sid => $entity) {
×
424
            $this->forgetEntity($entity);
×
425

426
            unset($this->forgetEntities[$sid]);
×
427
        }
428
    }
429

430
    /**
431
     * Cache results.
432
     *
433
     * @param string                   $cacheKey
434
     * @param int|Entity|Entity[]|null $results
435
     *
436
     * @return void
437
     */
438
    protected function cacheResults(string $cacheKey, Entity|array|int|null $results): void
439
    {
440
        $id     = $this->id;
×
441
        $tags   = $id !== null && $id !== ''
×
442
            ? [(string) $this->id]
×
443
            : [];
×
444
        $tags[] = $this->entity;
×
445

446
        if (is_array($results)) {
×
447
            $tags = [];
×
448

449
            foreach ($results as $result) {
×
450
                $tags[] = $this->getEntityCacheKey($result);
×
451
            }
452
        }
453

454
        $this->store->forever($cacheKey, base64_encode(serialize($results)));
×
455

456
        $this->store->getTagger(...$tags)->tag($cacheKey);
×
457
    }
458
}
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