• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orisai / object-mapper / 13365744880

17 Feb 2025 12:36AM UTC coverage: 86.69%. First build
13365744880

push

github

mabar
DateTimeRule: fix iso_compat format with second fractions with more than 3 digits

9 of 10 new or added lines in 1 file covered. (90.0%)

2957 of 3411 relevant lines covered (86.69%)

191.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.36
/src/Rules/DateTimeRule.php
1
<?php declare(strict_types = 1);
2

3
namespace Orisai\ObjectMapper\Rules;
4

5
use DateTime;
6
use DateTimeImmutable;
7
use DateTimeInterface;
8
use DateTimeZone;
9
use Nette\Utils\Validators;
10
use Orisai\Exceptions\Logic\InvalidArgument;
11
use Orisai\ObjectMapper\Args\Args;
12
use Orisai\ObjectMapper\Args\ArgsChecker;
13
use Orisai\ObjectMapper\Exception\ValueDoesNotMatch;
14
use Orisai\ObjectMapper\Meta\Context\MetaFieldContext;
15
use Orisai\ObjectMapper\Processing\Context\DynamicContext;
16
use Orisai\ObjectMapper\Processing\Context\PropertyContext;
17
use Orisai\ObjectMapper\Processing\Context\ServicesContext;
18
use Orisai\ObjectMapper\Processing\Value;
19
use Orisai\ObjectMapper\Types\SimpleValueType;
20
use ReflectionClass;
21
use Throwable;
22
use function assert;
23
use function is_a;
24
use function is_int;
25
use function is_string;
26
use function preg_replace;
27
use function sprintf;
28
use function strpos;
29
use function substr;
30
use const PHP_VERSION_ID;
31

32
/**
33
 * @implements Rule<DateTimeArgs>
34
 */
35
final class DateTimeRule implements Rule
36
{
37

38
        public const
39
                Format = 'format',
40
                ClassName = 'class';
41

42
        public const FormatTimestamp = 'timestamp',
43
                FormatAny = 'any',
44
                FormatIsoCompat = 'iso_compat';
45

46
        private const JsIsoFormat = 'Y-m-d\TH:i:s.u\Z';
47

48
        public function resolveArgs(array $args, MetaFieldContext $context): DateTimeArgs
49
        {
50
                $checker = new ArgsChecker($args, self::class);
56✔
51
                $checker->checkAllowedArgs([self::Format, self::ClassName]);
56✔
52

53
                $format = self::FormatIsoCompat;
56✔
54
                if ($checker->hasArg(self::Format)) {
56✔
55
                        $format = $checker->checkString(self::Format);
40✔
56
                }
57

58
                $type = DateTimeImmutable::class;
56✔
59
                if ($checker->hasArg(self::ClassName)) {
56✔
60
                        $type = $args[self::ClassName];
40✔
61

62
                        if (
63
                                !is_string($type)
40✔
64
                                || !is_a($type, DateTimeInterface::class, true)
40✔
65
                                || (new ReflectionClass($type))->isAbstract()
40✔
66
                        ) {
67
                                throw InvalidArgument::create()
×
68
                                        ->withMessage($checker->formatMessage(
×
69
                                                sprintf(
×
70
                                                        '%s or %s or their non-abstract child class',
71
                                                        DateTimeImmutable::class,
72
                                                        DateTime::class,
×
73
                                                ),
74
                                                self::ClassName,
×
75
                                                $type,
76
                                        ));
77
                        }
78
                }
79

80
                return new DateTimeArgs($type, $format);
56✔
81
        }
82

83
        public function getArgsType(): string
84
        {
85
                return DateTimeArgs::class;
24✔
86
        }
87

88
        /**
89
         * @param mixed        $value
90
         * @param DateTimeArgs $args
91
         * @return DateTimeImmutable|DateTime|string|int
92
         * @throws ValueDoesNotMatch
93
         */
94
        public function processValue(
95
                $value,
96
                Args $args,
97
                ServicesContext $services,
98
                PropertyContext $property,
99
                DynamicContext $dynamic
100
        )
101
        {
102
                if (!is_string($value) && !is_int($value)) {
192✔
103
                        throw ValueDoesNotMatch::create($this->createType($args, $services, $dynamic), Value::of($value));
32✔
104
                }
105

106
                $format = $args->format;
160✔
107
                $isTimestamp = false;
160✔
108

109
                if ($format === self::FormatTimestamp || ($format === self::FormatAny && Validators::isNumericInt($value))) {
160✔
110
                        $isTimestamp = true;
40✔
111
                }
112

113
                $stringValue = is_int($value) ? (string) $value : $value;
160✔
114
                $classType = $args->class;
160✔
115

116
                if ($isTimestamp) {
160✔
117
                        $datetime = $classType::createFromFormat('U', $stringValue);
40✔
118
                } elseif ($format === self::FormatAny) {
120✔
119
                        try {
120
                                $datetime = new $classType($stringValue);
40✔
121
                        } catch (Throwable $exception) {
8✔
122
                                $type = $this->createType($args, $services, $dynamic);
8✔
123
                                if ($type->hasParameter('format')) {
8✔
124
                                        $type->markParameterInvalid('format');
×
125
                                }
126

127
                                $message = $exception->getMessage();
8✔
128
                                if (PHP_VERSION_ID < 8_01_00) {
8✔
129
                                        // Drop 'DateTimeImmutable::__construct(): ' from message start
130
                                        $pos = strpos($message, ' ');
3✔
131
                                        assert($pos !== false);
3✔
132
                                        $message = substr($message, $pos + 1);
3✔
133
                                }
134

135
                                $type->addKeyParameter($message);
8✔
136
                                $type->markParameterInvalid($message);
8✔
137

138
                                throw ValueDoesNotMatch::create($type, Value::of($value));
24✔
139
                        }
140
                } elseif ($format === self::FormatIsoCompat) {
80✔
141
                        if ($stringValue !== '' && substr($stringValue, -1) === 'Z') {
56✔
142
                                // Truncate fractional seconds beyond 6 digits if present.
143
                                // Example: 2023-07-14T13:52:32.489932695Z -> 2023-07-14T13:52:32.489932Z
144
                                $truncatedValue = preg_replace('/\.(\d{6})\d+Z$/', '.$1Z', $stringValue);
32✔
145
                                $datetime = $truncatedValue === null
32✔
NEW
146
                                        ? false
×
147
                                        : $classType::createFromFormat(
32✔
148
                                                self::JsIsoFormat,
32✔
149
                                                $truncatedValue,
28✔
150
                                                new DateTimeZone('UTC'),
32✔
151
                                        );
28✔
152
                        } else {
153
                                $datetime = $classType::createFromFormat(
36✔
154
                                        DateTimeInterface::ATOM,
40✔
155
                                        $stringValue,
33✔
156
                                );
33✔
157
                        }
158
                } else {
159
                        $datetime = $classType::createFromFormat($format, $stringValue);
24✔
160
                }
161

162
                if ($datetime === false) {
152✔
163
                        $errors = $args->isImmutable()
24✔
164
                                ? DateTimeImmutable::getLastErrors()
24✔
165
                                : DateTime::getLastErrors();
6✔
166
                        assert($errors !== false);
24✔
167

168
                        $type = $this->createType($args, $services, $dynamic);
24✔
169
                        if ($type->hasParameter('format')) {
24✔
170
                                $type->markParameterInvalid('format');
16✔
171
                        }
172

173
                        foreach ($errors['errors'] as $error) {
24✔
174
                                $type->addKeyParameter($error);
24✔
175
                                $type->markParameterInvalid($error);
24✔
176
                        }
177

178
                        throw ValueDoesNotMatch::create($type, Value::of($value));
24✔
179
                }
180

181
                return $dynamic->shouldInitializeObjects()
128✔
182
                        ? $datetime
120✔
183
                        : $value;
128✔
184
        }
185

186
        public function createType(
187
                Args $args,
188
                ServicesContext $services,
189
                DynamicContext $dynamic
190
        ): SimpleValueType
191
        {
192
                if ($args->format === self::FormatTimestamp) {
88✔
193
                        return new SimpleValueType('timestamp');
16✔
194
                }
195

196
                $type = new SimpleValueType('datetime');
72✔
197

198
                $format = $args->format;
72✔
199
                if ($format === self::FormatIsoCompat) {
72✔
200
                        $format = DateTimeInterface::ATOM . ' | ' . self::JsIsoFormat;
8✔
201
                }
202

203
                if ($format !== self::FormatAny) {
72✔
204
                        $type->addKeyValueParameter('format', $format);
32✔
205
                }
206

207
                return $type;
72✔
208
        }
209

210
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc