Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Meta
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
4 / 4
24
100.00% covered (success)
100.00%
1 / 1
 apply
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
9
 textMap
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
9
 focal
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 clamp
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
7/**
8 * The editable slice of an asset's `meta` bag: localized text
9 * (`alt`/`title`/`caption` as locale => string maps), a scalar `credit`
10 * line, and an image `focal` point `{x, y}` clamped to 0..1. Empty
11 * values are dropped so the stored bag never accumulates blank keys, and
12 * keys the panel does not manage are carried through untouched.
13 */
14final class Meta
15{
16    /** Localized text keys, stored as locale => string maps. */
17    private const array TEXT = ['alt', 'title', 'caption'];
18
19    /**
20     * Merge a submitted editable patch over the stored bag. The managed
21     * keys are cleared first, so a field the user emptied is removed
22     * rather than left at its previous value.
23     *
24     * @param array<string, mixed> $stored the asset's current meta
25     * @param list<string> $localeIds locale ids accepted as text-map keys
26     * @return array<string, mixed>
27     */
28    public static function apply(array $stored, mixed $input, array $localeIds, bool $image): array
29    {
30        foreach ([...self::TEXT, 'credit', 'focal'] as $key) {
31            unset($stored[$key]);
32        }
33
34        $input = is_array($input) ? $input : [];
35
36        foreach (self::TEXT as $key) {
37            $map = self::textMap($input[$key] ?? null, $localeIds);
38
39            if ($map !== []) {
40                $stored[$key] = $map;
41            }
42        }
43
44        $credit = is_string($input['credit'] ?? null) ? trim($input['credit']) : '';
45
46        if ($credit !== '') {
47            $stored['credit'] = $credit;
48        }
49
50        if ($image) {
51            $focal = self::focal($input['focal'] ?? null);
52
53            if ($focal !== null) {
54                $stored['focal'] = $focal;
55            }
56        }
57
58        return $stored;
59    }
60
61    /**
62     * @param list<string> $localeIds
63     * @return array<string, string>
64     */
65    private static function textMap(mixed $value, array $localeIds): array
66    {
67        if (!is_array($value)) {
68            return [];
69        }
70
71        $allowed = $localeIds === [] ? null : array_fill_keys($localeIds, true);
72        $map = [];
73
74        foreach ($value as $locale => $text) {
75            if (!is_string($locale) || $allowed !== null && !isset($allowed[$locale])) {
76                continue;
77            }
78
79            $text = is_string($text) ? trim($text) : '';
80
81            if ($text !== '') {
82                $map[$locale] = $text;
83            }
84        }
85
86        return $map;
87    }
88
89    /** @return array{x: float, y: float}|null */
90    private static function focal(mixed $value): ?array
91    {
92        if (
93            !is_array($value)
94            || !isset($value['x'], $value['y'])
95            || !is_numeric($value['x'])
96            || !is_numeric($value['y'])
97        ) {
98            return null;
99        }
100
101        return [
102            'x' => self::clamp((float) $value['x']),
103            'y' => self::clamp((float) $value['y']),
104        ];
105    }
106
107    private static function clamp(float $n): float
108    {
109        return max(0.0, min(1.0, $n));
110    }
111}