Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
Repository
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
7 / 7
18
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 collectUids
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 preload
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 get
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 add
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fetch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromRow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Assets;
6
7use Celema\Quma\Database;
8use Cosray\Config;
9
10/**
11 * Asset catalog access with a per-instance cache. One instance lives on
12 * the request Context, so every value object of a request shares one
13 * batch-loaded uid map instead of issuing per-item queries.
14 */
15class Repository
16{
17    /** @var array<string, ?Asset> */
18    protected array $cache = [];
19
20    public function __construct(
21        protected readonly Database $db,
22        protected readonly Config $config,
23    ) {}
24
25    /**
26     * Collect asset uids referenced by media items in a content array.
27     * Media items are exactly `{uid}` or `{uid, meta}`; arrays with more
28     * keys (blocks, entries) are descended into instead.
29     *
30     * @return list<string>
31     */
32    public static function collectUids(array $content): array
33    {
34        $uids = [];
35        $walk = static function (array $data) use (&$walk, &$uids): void {
36            if (
37                is_string($data['uid'] ?? null)
38                && $data['uid'] !== ''
39                && array_diff(array_keys($data), ['uid', 'meta']) === []
40            ) {
41                $uids[$data['uid']] = true;
42
43                return;
44            }
45
46            foreach ($data as $value) {
47                if (!is_array($value)) {
48                    continue;
49                }
50
51                $walk($value);
52            }
53        };
54        $walk($content);
55
56        return array_keys($uids);
57    }
58
59    /** @param list<string> $uids */
60    public function preload(array $uids): void
61    {
62        $missing = [];
63
64        foreach ($uids as $uid) {
65            if (array_key_exists($uid, $this->cache)) {
66                continue;
67            }
68
69            $missing[] = $uid;
70        }
71
72        if ($missing === []) {
73            return;
74        }
75
76        foreach ($this->fetch($missing) as $row) {
77            $this->add($this->fromRow($row));
78        }
79
80        // Remember misses so unknown uids are not refetched per item.
81        foreach ($missing as $uid) {
82            $this->cache[$uid] ??= null;
83        }
84    }
85
86    public function get(string $uid): ?Asset
87    {
88        if (!array_key_exists($uid, $this->cache)) {
89            $this->preload([$uid]);
90        }
91
92        return $this->cache[$uid];
93    }
94
95    /** Seed the cache, e.g. after an upload or in tests. */
96    public function add(Asset $asset): void
97    {
98        $this->cache[$asset->uid] = $asset;
99    }
100
101    /** @param list<string> $uids */
102    protected function fetch(array $uids): array
103    {
104        return $this->db->assets->byUids(['uids' => $uids])->all();
105    }
106
107    protected function fromRow(array $row): Asset
108    {
109        return Asset::fromRow($row, $this->config);
110    }
111}