Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.15% covered (success)
93.15%
68 / 73
80.00% covered (warning)
80.00%
16 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
File
93.15% covered (success)
93.15%
68 / 73
80.00% covered (warning)
80.00%
16 / 20
48.74
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __toString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 title
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 caption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 url
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 publicPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 filename
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mimetype
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 raw
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 json
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 count
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 asset
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 files
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
9.24
 fileItem
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 effectiveFiles
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 hasFile
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 getFileName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 textValue
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 textFrom
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Value;
6
7use Cosray\Assets\Asset;
8use Cosray\Exception\RuntimeException;
9use Cosray\Field\Capability\Translatable;
10use Cosray\Field\Field;
11use Cosray\Field\Owner;
12use Cosray\Schema\TranslateMode;
13use Override;
14
15/**
16 * @property-read Field&Translatable $field
17 */
18class File extends Value
19{
20    public function __construct(
21        Owner $owner,
22        Field&Translatable $field,
23        ValueContext $context,
24        protected int $index = 0,
25    ) {
26        parent::__construct($owner, $field, $context);
27    }
28
29    #[Override]
30    public function __toString(): string
31    {
32        return $this->publicPath();
33    }
34
35    public function title(): string
36    {
37        return $this->textValue('title', $this->index);
38    }
39
40    public function caption(): string
41    {
42        return $this->textValue('caption', $this->index);
43    }
44
45    public function url(): string
46    {
47        $url = $this->owner->origin() . $this->publicPath();
48
49        if ($url = filter_var($url, FILTER_VALIDATE_URL)) {
50            return $url;
51        }
52
53        throw new RuntimeException('Invalid file url');
54    }
55
56    /**
57     * Root-relative URL of the asset. Equal to the file's path below the
58     * public directory, so the web server serves it without PHP.
59     */
60    public function publicPath(): string
61    {
62        return $this->asset($this->index)?->path() ?? '';
63    }
64
65    public function filename(): string
66    {
67        return $this->getFileName($this->index);
68    }
69
70    public function mimetype(): string
71    {
72        return $this->asset($this->index)->mime ?? '';
73    }
74
75    /** @return ?array<mixed> */
76    #[Override]
77    public function raw(): ?array
78    {
79        return $this->fileItem($this->index);
80    }
81
82    /** @return ?array<string, mixed> null without a file or when its asset is gone */
83    #[Override]
84    public function json(): ?array
85    {
86        $asset = $this->asset($this->index);
87
88        if ($asset === null) {
89            return null;
90        }
91
92        return [
93            'uid' => $asset->uid,
94            'url' => $this->publicPath(),
95            'filename' => $asset->filename,
96            'mime' => $asset->mime,
97            'bytes' => $asset->bytes,
98            'title' => $this->title(),
99            'caption' => $this->caption(),
100        ];
101    }
102
103    public function count(): int
104    {
105        return count($this->files());
106    }
107
108    #[Override]
109    public function isset(): bool
110    {
111        return $this->fileItem($this->index) !== null;
112    }
113
114    protected function asset(int $index): ?Asset
115    {
116        $uid = (string) ($this->fileItem($index)['uid'] ?? '');
117
118        return $uid === '' ? null : $this->owner->assets()->get($uid);
119    }
120
121    /** @return array<mixed> */
122    protected function files(): array
123    {
124        $value = $this->data['value'] ?? [];
125
126        if (
127            !isset($this->data['type'])
128            && isset($this->data['files'])
129            && is_array($this->data['files'])
130        ) {
131            if (
132                $this->field->translateMode() === TranslateMode::Asymmetric
133                && !array_is_list($this->data['files'])
134            ) {
135                return $this->effectiveFiles($this->data['files']);
136            }
137
138            return $this->data['files'];
139        }
140
141        if (!is_array($value)) {
142            return [];
143        }
144
145        if ($this->field->translateMode() === TranslateMode::Asymmetric) {
146            return $this->effectiveFiles($value);
147        }
148
149        $files = $value[Field::NEUTRAL_LOCALE] ?? [];
150
151        return is_array($files) ? $files : [];
152    }
153
154    /** @return ?array<mixed> */
155    protected function fileItem(int $index): ?array
156    {
157        $item = $this->files()[$index] ?? null;
158
159        return is_array($item) ? $item : null;
160    }
161
162    /**
163     * @param array<string, mixed> $map
164     * @return array<mixed>
165     */
166    protected function effectiveFiles(array $map): array
167    {
168        $locale = $this->locale;
169
170        while ($locale) {
171            $files = $map[$locale->id] ?? null;
172
173            if ($this->hasFile($files)) {
174                return $files;
175            }
176
177            $locale = $locale->fallback();
178        }
179
180        $files = $map[Field::NEUTRAL_LOCALE] ?? null;
181
182        return $this->hasFile($files) ? $files : [];
183    }
184
185    protected function hasFile(mixed $files): bool
186    {
187        if (!is_array($files)) {
188            return false;
189        }
190
191        foreach ($files as $file) {
192            if (is_array($file) && ($file['uid'] ?? null)) {
193                return true;
194            }
195        }
196
197        return false;
198    }
199
200    protected function getFileName(int $index): string
201    {
202        return $this->asset($index)->filename ?? '';
203    }
204
205    /**
206     * Per-use meta wins only when it is non-empty for the effective
207     * locale; otherwise the asset's catalog default applies.
208     */
209    protected function textValue(string $key, int $index): string
210    {
211        $item = $this->fileItem($index) ?? [];
212        $value = $item['meta'][$key] ?? null;
213
214        if (!is_array($value) && !isset($this->data['type']) && array_key_exists($key, $item)) {
215            $value = $item[$key];
216        }
217
218        $result = $this->textFrom($value);
219
220        if ($result !== '') {
221            return $result;
222        }
223
224        return $this->textFrom($this->asset($index)?->metaMap($key));
225    }
226
227    protected function textFrom(mixed $value): string
228    {
229        if (!is_array($value)) {
230            return is_string($value) || is_numeric($value) ? (string) $value : '';
231        }
232
233        $value = $this->effective($value);
234
235        if (is_string($value) || is_numeric($value)) {
236            return (string) $value;
237        }
238
239        return '';
240    }
241}