Skip to content

Commit 83bd7c1

Browse files
committed
Add support for datetime in strict mode
1 parent afbaab2 commit 83bd7c1

2 files changed

Lines changed: 115 additions & 2 deletions

File tree

src/JsonMapper.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,8 @@ public function map($json, $object)
301301
// but only a flat type (i.e. string, int)
302302
if ($this->bStrictObjectTypeChecking
303303
&& !is_subclass_of($type, \BackedEnum::class)
304+
&& (!is_subclass_of($type, \DateTimeInterface::class)
305+
|| !$this->isSafeDateTime($jvalue))
304306
) {
305307
throw new JsonMapper_Exception(
306308
'JSON property "' . $key . '" must be an object, '
@@ -343,7 +345,7 @@ public function map($json, $object)
343345
* @param $type Type name (simple type or class name)
344346
* @param $strNs Base namespace that gets prepended to the type name
345347
*
346-
* @return Fully-qualified type name with namespace
348+
* @return ?string Fully-qualified type name with namespace
347349
*/
348350
protected function getFullNamespace(?string $type, string $strNs): ?string
349351
{
@@ -455,6 +457,8 @@ public function mapArray(
455457
$array[$key] = $jvalue;
456458
} else if ($this->bStrictObjectTypeChecking
457459
&& !is_subclass_of($class, \BackedEnum::class)
460+
&& (!is_subclass_of($class, \DateTimeInterface::class)
461+
|| !$this->isSafeDateTime($jvalue))
458462
) {
459463
throw new JsonMapper_Exception(
460464
'JSON property'
@@ -708,6 +712,29 @@ protected function createInstance(
708712
}
709713
}
710714

715+
/**
716+
* Checks if the given JSON value is a strict ISO 8601 / RFC 3339 date string.
717+
*
718+
* @param mixed $jvalue JSON value being mapped
719+
*
720+
* @return bool
721+
*/
722+
protected function isSafeDateTime(mixed $jvalue): bool
723+
{
724+
$pattern = '/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/';
725+
if (!preg_match($pattern, $jvalue)) {
726+
return false;
727+
}
728+
try {
729+
new \DateTimeImmutable($jvalue);
730+
} catch (\Exception $e) {
731+
return false;
732+
}
733+
$errors = \DateTimeImmutable::getLastErrors();
734+
return !is_array($errors)
735+
|| (empty($errors['warnings']) && empty($errors['errors']));
736+
}
737+
711738
/**
712739
* Get the mapped class/type name for this class.
713740
* Returns the incoming classname if not mapped.
@@ -746,7 +773,7 @@ protected function getMappedType(?string $type, mixed $jvalue = null): ?string
746773
*
747774
* @param $type type name from gettype()
748775
*
749-
* @return boole True if it is a simple PHP type
776+
* @return bool True if it is a simple PHP type
750777
*
751778
* @see isFlatType()
752779
*/

tests/DateTime_Strict_Test.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
/**
3+
* Unit tests for JsonMapper's DateTime handling under strict object type checking.
4+
*
5+
* @category Tests
6+
* @package JsonMapper
7+
* @license OSL-3.0 http://opensource.org/licenses/osl-3.0
8+
* @link https://github.com/cweiske/jsonmapper
9+
*/
10+
class DateTime_Strict_Test extends \PHPUnit\Framework\TestCase
11+
{
12+
public static function validFormatData()
13+
{
14+
return [
15+
'date only' => ['2024-01-15'],
16+
'space separator' => ['2024-01-15 12:30:45'],
17+
'iso with colon offset' => ['2024-01-15T12:30:45+00:00'],
18+
'rfc 3339 fractional Z' => ['2024-01-15T12:30:45.123Z'],
19+
'compact offset' => ['2024-01-15T12:30:45+0100'],
20+
];
21+
}
22+
23+
/**
24+
* @dataProvider validFormatData
25+
*/
26+
public function testMapValidFormat($value)
27+
{
28+
$jm = new JsonMapper();
29+
$jm->bStrictObjectTypeChecking = true;
30+
$sn = $jm->map(
31+
json_decode("{\"datetime\":\"$value\"}"),
32+
new JsonMapperTest_Object()
33+
);
34+
$this->assertInstanceOf(DateTime::class, $sn->datetime);
35+
}
36+
37+
public static function invalidFormatData()
38+
{
39+
return [
40+
'relative string' => ['now'],
41+
'wrong format' => ['2024/01/15'],
42+
'overflow date' => ['2024-02-30'],
43+
];
44+
}
45+
46+
/**
47+
* @dataProvider invalidFormatData
48+
*/
49+
public function testMapInvalidFormat($value)
50+
{
51+
$jm = new JsonMapper();
52+
$jm->bStrictObjectTypeChecking = true;
53+
$this->expectException(JsonMapper_Exception::class);
54+
$this->expectExceptionMessage('JSON property "datetime" must be an object, string given');
55+
$sn = $jm->map(
56+
json_decode("{\"datetime\":\"$value\"}"),
57+
new JsonMapperTest_Object()
58+
);
59+
}
60+
61+
public function testMapArrayValidFormat()
62+
{
63+
$jm = new JsonMapper();
64+
$jm->bStrictObjectTypeChecking = true;
65+
$sn = $jm->map(
66+
json_decode("{\"typedSimpleArray\":[\"{$this->validFormatData()['date only'][0]}\",\"{$this->validFormatData()['space separator'][0]}\"]}"),
67+
new JsonMapperTest_Array()
68+
);
69+
$this->assertIsArray($sn->typedSimpleArray);
70+
$this->assertCount(2, $sn->typedSimpleArray);
71+
$this->assertInstanceOf(DateTime::class, $sn->typedSimpleArray[0]);
72+
$this->assertInstanceOf(DateTime::class, $sn->typedSimpleArray[1]);
73+
}
74+
75+
public function testMapArrayInvalidFormat()
76+
{
77+
$jm = new JsonMapper();
78+
$jm->bStrictObjectTypeChecking = true;
79+
$this->expectException(JsonMapper_Exception::class);
80+
$this->expectExceptionMessage('JSON property "typedSimpleArray[1]" must be an object, string given');
81+
$sn = $jm->map(
82+
json_decode("{\"typedSimpleArray\":[\"{$this->validFormatData()['date only'][0]}\",\"{$this->invalidFormatData()['relative string'][0]}\"]}"),
83+
new JsonMapperTest_Array()
84+
);
85+
}
86+
}

0 commit comments

Comments
 (0)