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