Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.24% covered (success)
95.24%
40 / 42
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
DateTimeConverter
95.24% covered (success)
95.24%
40 / 42
80.00% covered (warning)
80.00%
4 / 5
20
0.00% covered (danger)
0.00%
0 / 1
 convert
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 isDateTime
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 field
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 value
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
4.25
 timezone
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Migration;
6
7use Cosray\DateTime\Codec;
8use Cosray\Exception\RuntimeException;
9use Cosray\Field\DateTime;
10use Cosray\Field\Field;
11use DateTimeZone;
12
13final class DateTimeConverter
14{
15    /**
16     * @param array<mixed> $data
17     * @return array<mixed>
18     */
19    public function convert(array $data, string $path = 'content'): array
20    {
21        if ($this->isDateTime($data)) {
22            return $this->field($data, $path);
23        }
24
25        foreach ($data as $key => $value) {
26            if (is_array($value)) {
27                $data[$key] = $this->convert($value, $path . '.' . $key);
28            }
29        }
30
31        return $data;
32    }
33
34    /** @param array<mixed> $data */
35    private function isDateTime(array $data): bool
36    {
37        $type = $data['type'] ?? null;
38
39        if (!is_string($type)) {
40            return false;
41        }
42
43        // History snapshots can retain aliases and retired package namespaces.
44        return $type === 'datetime' || $type === DateTime::class || str_ends_with($type, '\\Field\\DateTime');
45    }
46
47    /**
48     * @param array<mixed> $data
49     * @return array<mixed>
50     */
51    private function field(array $data, string $path): array
52    {
53        $value = $data['value'] ?? null;
54
55        if (is_array($value)) {
56            foreach ($value as $locale => $datetime) {
57                $value[$locale] = $this->value(
58                    $datetime,
59                    $this->timezone($data, is_string($locale) ? $locale : Field::NEUTRAL_LOCALE, $path),
60                    $path . '.value.' . $locale,
61                );
62            }
63        } else {
64            $value = $this->value(
65                $value,
66                $this->timezone($data, Field::NEUTRAL_LOCALE, $path),
67                $path . '.value',
68            );
69        }
70
71        $data['value'] = $value;
72
73        return $data;
74    }
75
76    private function value(mixed $value, DateTimeZone $timezone, string $path): mixed
77    {
78        if ($value === null || $value === '') {
79            return $value;
80        }
81
82        if (!is_string($value)) {
83            throw new RuntimeException("Invalid date/time value at {$path}");
84        }
85
86        return (
87            Codec::fromLegacy($value, $timezone)
88                ?? throw new RuntimeException("Invalid date/time value '{$value}' at {$path}")
89        );
90    }
91
92    /** @param array<mixed> $data */
93    private function timezone(array $data, string $locale, string $path): DateTimeZone
94    {
95        $value = $data['meta']['timezone'] ?? $data['timezone'] ?? null;
96
97        if (is_array($value)) {
98            $value = $value[$locale] ?? $value[Field::NEUTRAL_LOCALE] ?? null;
99        }
100
101        if ($value === null || $value === '') {
102            return Codec::utc();
103        }
104
105        return (
106            Codec::timezone($value)
107                ?? throw new RuntimeException("Invalid timezone at {$path}.meta.timezone.{$locale}")
108        );
109    }
110}