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

valkyrjaio / valkyrja / 13047041070

30 Jan 2025 06:43AM UTC coverage: 47.621% (+0.2%) from 47.422%
13047041070

push

github

MelechMizrachi
PHPStan level 7 and 8.

168 of 1038 new or added lines in 111 files covered. (16.18%)

444 existing lines in 45 files now uncovered.

5195 of 10909 relevant lines covered (47.62%)

18.83 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
 * @implements Contract<Entity>
52
 */
53
class CacheRepository extends Repository implements Contract
54
{
55
    /**
56
     * The cache store.
57
     *
58
     * @var CacheDriver
59
     */
60
    protected CacheDriver $store;
61

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

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

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

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

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

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

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

118
        return $this;
×
119
    }
120

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

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

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

140
        return $this;
×
141
    }
142

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

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

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

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

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

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

NEW
170
                return $results;
×
NEW
171
            } catch (Throwable) {
×
172
            }
173

174
            // Remove the bad cache
NEW
175
            $this->store->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

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

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

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

222
        $this->store->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) {
×
NEW
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

366
        $this->store->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 . $entity->__get($entity::getIdField());
×
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

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

446
        $this->store->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