Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.00% covered (success)
98.00%
49 / 50
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
MultiImageTransformer
97.96% covered (success)
97.96%
48 / 49
83.33% covered (warning)
83.33%
5 / 6
29
0.00% covered (danger)
0.00%
0 / 1
 supports
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 transformRead
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 transformWrite
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 parseArray
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 parseJsonArray
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 filterAndNormalize
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
10
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Engine\Application\Transformer;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Domain\Model\FieldMetadata;
12
13/**
14 * Multi Image Gallery UiType Transformer.
15 *
16 * Handles UiType: multi_image.
17 * Normalizes, validates and serializes arrays of uploaded image metadata and thumbnails.
18 *
19 * @package App\Core\Engine\Application\Transformer
20 */
21final class MultiImageTransformer implements UiTypeTransformerInterface
22{
23    /** {@inheritdoc} */
24    public function supports(string $uitypeName): bool
25    {
26        return in_array($uitypeName, ['multi_image', 'images_gallery', 'media_gallery'], true);
27    }
28
29    /** {@inheritdoc} */
30    public function transformRead(mixed $rawValue, FieldMetadata $field): string
31    {
32        if ($rawValue === null || $rawValue === '') {
33            return '[]';
34        }
35
36        $items = $this->parseArray($rawValue);
37        if (empty($items)) {
38            return '[]';
39        }
40
41        $normalized = $this->filterAndNormalize($items);
42        $encoded = json_encode($normalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
43
44        return $encoded !== false ? $encoded : '[]';
45    }
46
47    /** {@inheritdoc} */
48    public function transformWrite(mixed $inputValue, FieldMetadata $field): mixed
49    {
50        if ($inputValue === null || $inputValue === '' || $inputValue === '[]') {
51            return null;
52        }
53
54        $normalized = $this->filterAndNormalize($this->parseArray($inputValue));
55        if (empty($normalized)) {
56            return null;
57        }
58
59        $encoded = json_encode($normalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
60
61        return $encoded !== false ? $encoded : null;
62    }
63
64    /**
65     * Parses input into raw array representation.
66     *
67     * @param mixed $value Input data.
68     * @return array<int, mixed> Extracted items array.
69     */
70    private function parseArray(mixed $value): array
71    {
72        if (is_array($value)) {
73            return array_values($value);
74        }
75
76        if (is_string($value)) {
77            return $this->parseJsonArray($value);
78        }
79
80        return [];
81    }
82
83    /**
84     * @return array<int, mixed>
85     */
86    private function parseJsonArray(string $value): array
87    {
88        $trimmed = trim($value);
89        if ($trimmed === '' || $trimmed === '[]') {
90            return [];
91        }
92
93        $decoded = json_decode($trimmed, true);
94        return is_array($decoded) ? array_values($decoded) : [];
95    }
96
97    /**
98     * Filters and sanitizes image records, ensuring required fields and safe paths.
99     *
100     * @param array<int, mixed> $items Raw list of image items.
101     * @return array<int, array{path: string, thumb: string, name: string, size: int, mime: string}>
102     */
103    private function filterAndNormalize(array $items): array
104    {
105        $result = [];
106
107        foreach ($items as $item) {
108            if (!is_array($item)) {
109                continue;
110            }
111
112            $rawPath = trim((string) ($item['path'] ?? ''));
113            if ($rawPath === '' || str_contains($rawPath, '..')) {
114                continue;
115            }
116
117            $rawThumb = trim((string) ($item['thumb'] ?? ''));
118            if ($rawThumb === '' || str_contains($rawThumb, '..')) {
119                $rawThumb = $rawPath;
120            }
121
122            $rawName = trim((string) ($item['name'] ?? ''));
123            if ($rawName === '') {
124                $rawName = basename($rawPath);
125            }
126            $cleanName = (string) preg_replace('/[^\w\s\.\-\(\)]/u', '_', $rawName);
127
128            $rawMime = trim((string) ($item['mime'] ?? ''));
129            if ($rawMime === '') {
130                $rawMime = 'image/jpeg';
131            }
132
133            $result[] = [
134                'path'  => $rawPath,
135                'thumb' => $rawThumb,
136                'name'  => $cleanName !== '' ? $cleanName : 'image.jpg',
137                'size'  => max(0, (int) ($item['size'] ?? 0)),
138                'mime'  => $rawMime,
139            ];
140        }
141
142        return $result;
143    }
144}