Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
Scanner
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
8 / 8
31
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
 scan
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 fields
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
9
 rows
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 list
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 collect
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 items
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
6
 unique
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\References;
6
7use Cosray\Field;
8use Cosray\Richtext;
9
10/**
11 * Collects every asset and node uid referenced by stored node content:
12 * media field items (`{uid}`), Reference field targets, and the richtext
13 * carriers (`image.uid`, `link.asset`, `link.node`). The rows of Blocks
14 * and Entries fields are recursed into. Feeds the reference indexes.
15 */
16final class Scanner
17{
18    private readonly Field\Index $index;
19
20    public function __construct(?Field\Index $index = null)
21    {
22        $this->index = $index ?? Field\Index::withDefaults();
23    }
24
25    /** @return array{assets: list<string>, nodes: list<string>} */
26    public function scan(mixed $content): array
27    {
28        $assets = [];
29        $nodes = [];
30
31        if (is_array($content)) {
32            $this->fields($content, $assets, $nodes);
33            $richtext = Richtext\Scanner::scanContent($content);
34            $assets = [...$assets, ...$richtext['assets']];
35            $nodes = [...$nodes, ...$richtext['nodes']];
36        }
37
38        return ['assets' => $this->unique($assets), 'nodes' => $this->unique($nodes)];
39    }
40
41    /**
42     * @param list<string> $assets
43     * @param list<string> $nodes
44     * @param array<mixed> $content
45     */
46    private function fields(array $content, array &$assets, array &$nodes): void
47    {
48        foreach ($content as $field) {
49            if (!is_array($field) || !is_string($field['type'] ?? null)) {
50                continue;
51            }
52
53            $type = $this->index->resolve($field['type']);
54
55            if ($type === null) {
56                continue;
57            }
58
59            if (is_a($type, Field\Blocks::class, true) || is_a($type, Field\Entries::class, true)) {
60                $this->rows($field['value'] ?? null, $assets, $nodes);
61            } elseif (is_a($type, Field\Reference::class, true)) {
62                $this->collect($field['value'] ?? null, $nodes);
63            } elseif (is_a($type, Field\File::class, true)) {
64                $this->collect($field['value'] ?? null, $assets);
65            }
66        }
67    }
68
69    /**
70     * Typed rows carry their sub-fields under `fields`, per locale list.
71     *
72     * @param list<string> $assets
73     * @param list<string> $nodes
74     */
75    private function rows(mixed $value, array &$assets, array &$nodes): void
76    {
77        foreach (is_array($value) ? $value : [] as $rows) {
78            $this->list($rows, $assets, $nodes);
79        }
80    }
81
82    /**
83     * A split of a Blocks field holds its rows under `blocks`.
84     *
85     * @param list<string> $assets
86     * @param list<string> $nodes
87     */
88    private function list(mixed $rows, array &$assets, array &$nodes): void
89    {
90        foreach (is_array($rows) ? $rows : [] as $row) {
91            if (!is_array($row)) {
92                continue;
93            }
94
95            if (is_array($row['fields'] ?? null)) {
96                $this->fields($row['fields'], $assets, $nodes);
97            }
98
99            if (is_array($row['blocks'] ?? null)) {
100                $this->list($row['blocks'], $assets, $nodes);
101            }
102        }
103    }
104
105    /**
106     * Collect uids from a locale-keyed map of `{uid}` lists into $out.
107     *
108     * @param list<string> $out
109     */
110    private function collect(mixed $value, array &$out): void
111    {
112        foreach (is_array($value) ? $value : [] as $items) {
113            $this->items($items, $out);
114        }
115    }
116
117    /** @param list<string> $out */
118    private function items(mixed $items, array &$out): void
119    {
120        foreach (is_array($items) ? $items : [] as $item) {
121            $uid = is_array($item) ? $item['uid'] ?? null : null;
122
123            if (is_string($uid) && $uid !== '') {
124                $out[] = $uid;
125            }
126        }
127    }
128
129    /**
130     * @param list<string> $uids
131     * @return list<string>
132     */
133    private function unique(array $uids): array
134    {
135        $uids = array_values(array_unique($uids));
136        sort($uids);
137
138        return $uids;
139    }
140}