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     * @param array<mixed> $content
31     * @return list<string>
32     */
33    public static function collectUids(array $content): array
34    {
35        $uids = [];
36        $walk = static function (array $data) use (&$walk, &$uids): void {
37            if (
38                is_string($data['uid'] ?? null)
39                && $data['uid'] !== ''
40                && array_diff(array_keys($data), ['uid', 'meta']) === []
41            ) {
42                $uids[$data['uid']] = true;
43
44                return;
45            }
46
47            foreach ($data as $value) {
48                if (!is_array($value)) {
49                    continue;
50                }
51
52                $walk($value);
53            }
54        };
55        $walk($content);
56
57        return array_keys($uids);
58    }
59
60    /** @param list<string> $uids */
61    public function preload(array $uids): void
62    {
63        $missing = [];
64
65        foreach ($uids as $uid) {
66            if (array_key_exists($uid, $this->cache)) {
67                continue;
68            }
69
70            $missing[] = $uid;
71        }
72
73        if ($missing === []) {
74            return;
75        }
76
77        foreach ($this->fetch($missing) as $row) {
78            $this->add($this->fromRow($row));
79        }
80
81        // Remember misses so unknown uids are not refetched per item.
82        foreach ($missing as $uid) {
83            $this->cache[$uid] ??= null;
84        }
85    }
86
87    public function get(string $uid): ?Asset
88    {
89        if (!array_key_exists($uid, $this->cache)) {
90            $this->preload([$uid]);
91        }
92
93        return $this->cache[$uid];
94    }
95
96    /** Seed the cache, e.g. after an upload or in tests. */
97    public function add(Asset $asset): void
98    {
99        $this->cache[$asset->uid] = $asset;
100    }
101
102    /**
103     * @param list<string> $uids
104     * @return list<array<mixed>>
105     */
106    protected function fetch(array $uids): array
107    {
108        return $this->db->assets->byUids(['uids' => $uids])->all();
109    }
110
111    /** @param array<mixed> $row */
112    protected function fromRow(array $row): Asset
113    {
114        return Asset::fromRow($row, $this->config);
115    }
116}