diff --git a/doc/reference/annotations.rst b/doc/reference/annotations.rst index 2ada97776..1094db672 100644 --- a/doc/reference/annotations.rst +++ b/doc/reference/annotations.rst @@ -45,8 +45,8 @@ This annotation can be defined on a property to define the serialized name for a property. If this is not defined, the property will be translated from camel-case to a lower-cased underscored name, e.g. camelCase -> camel_case. -Note that this annotation is not used when you're using any other naming -strategy than the default configuration (which includes the +Note that this annotation is not used when you're using any other naming +strategy than the default configuration (which includes the ``SerializedNameAnnotationStrategy``). In order to re-enable the annotation, you will need to wrap your custom strategy with the ``SerializedNameAnnotationStrategy``. @@ -142,7 +142,7 @@ be called to retrieve, or set the value of the given property: $this->name = $name; } } - + .. note :: If you need only to serialize your data, you can avoid providing a setter by @@ -327,102 +327,104 @@ used when deserializing them. Available Types: -+----------------------------------------------------------+--------------------------------------------------+ -| Type | Description | -+==========================================================+==================================================+ -| boolean or bool | Primitive boolean | -+----------------------------------------------------------+--------------------------------------------------+ -| integer or int | Primitive integer | -+----------------------------------------------------------+--------------------------------------------------+ -| double or float | Primitive double | -+----------------------------------------------------------+--------------------------------------------------+ -| string | Primitive string | -+----------------------------------------------------------+--------------------------------------------------+ -| array | An array with arbitrary keys, and values. | -+----------------------------------------------------------+--------------------------------------------------+ -| array | A list of type T (T can be any available type). | -| | Examples: | -| | array, array, etc. | -+----------------------------------------------------------+--------------------------------------------------+ -| array | A map of keys of type K to values of type V. | -| | Examples: array, | -| | array, etc. | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTime | PHP's DateTime object (default format*/timezone) | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTime<'format'> | PHP's DateTime object (custom format/default | -| | timezone) | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTime<'format', 'zone'> | PHP's DateTime object (custom format/timezone) | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTime<'format', 'zone', 'deserializeFormat'> | PHP's DateTime object (custom format/timezone, | -| | deserialize format). If you do not want to | -| | specify a specific timezone, use an empty | -| | string (''). | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTimeImmutable | PHP's DateTimeImmutable object (default format*/ | -| | timezone) | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTimeImmutable<'format'> | PHP's DateTimeImmutable object (custom format/ | -| | default timezone) | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTimeImmutable<'format', 'zone'> | PHP's DateTimeImmutable object (custom format/ | -| | timezone) | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTimeImmutable<'format', 'zone', 'deserializeFormat'> | PHP's DateTimeImmutable object (custom format/ | -| | timezone/deserialize format). If you do not want | -| | to specify a specific timezone, use an empty | -| | string (''). | -+----------------------------------------------------------+--------------------------------------------------+ -| DateInterval | PHP's DateInterval object using ISO 8601 format | -+----------------------------------------------------------+--------------------------------------------------+ -| T | Where T is a fully qualified class name. | -+----------------------------------------------------------+--------------------------------------------------+ -| iterable | Similar to array. Will always be deserialized | -| | into array as implementation info is lost during | -| | serialization. | -+----------------------------------------------------------+--------------------------------------------------+ -| iterable | Similar to array. Will always be deserialized | -| | into array as implementation info is lost during | -| | serialization. | -+----------------------------------------------------------+--------------------------------------------------+ -| iterable | Similar to array. Will always be | -| | deserialized into array as implementation info | -| | is lost during serialization. | -+----------------------------------------------------------+--------------------------------------------------+ -| ArrayCollection | Similar to array, but will be deserialized | -| | into Doctrine's ArrayCollection class. | -+----------------------------------------------------------+--------------------------------------------------+ -| ArrayCollection | Similar to array, but will be deserialized | -| | into Doctrine's ArrayCollection class. | -+----------------------------------------------------------+--------------------------------------------------+ -| Generator | Similar to array, but will be deserialized | -| | into Generator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| Generator | Similar to array, but will be deserialized | -| | into Generator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| Generator | Similar to array, but will be deserialized | -| | into Generator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| ArrayIterator | Similar to array, but will be deserialized | -| | into ArrayIterator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| ArrayIterator | Similar to array, but will be deserialized | -| | into ArrayIterator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| ArrayIterator | Similar to array, but will be deserialized | -| | into ArrayIterator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| Iterator | Similar to array, but will be deserialized | -| | into ArrayIterator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| Iterator | Similar to array, but will be deserialized | -| | into ArrayIterator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| Iterator | Similar to array, but will be deserialized | -| | into ArrayIterator class. | -+----------------------------------------------------------+--------------------------------------------------+ ++------------------------------------------------------------+--------------------------------------------------+ +| Type | Description | ++============================================================+==================================================+ +| boolean or bool | Primitive boolean | ++------------------------------------------------------------+--------------------------------------------------+ +| integer or int | Primitive integer | ++------------------------------------------------------------+--------------------------------------------------+ +| double or float | Primitive double | ++------------------------------------------------------------+--------------------------------------------------+ +| string | Primitive string | ++------------------------------------------------------------+--------------------------------------------------+ +| array | An array with arbitrary keys, and values. | ++------------------------------------------------------------+--------------------------------------------------+ +| array | A list of type T (T can be any available type). | +| | Examples: | +| | array, array, etc. | ++------------------------------------------------------------+--------------------------------------------------+ +| array | A map of keys of type K to values of type V. | +| | Examples: array, | +| | array, etc. | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTime | PHP's DateTime object (default format*/timezone) | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTime<'format'> | PHP's DateTime object (custom format/default | +| | timezone). | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTime<'format', 'zone'> | PHP's DateTime object (custom format/timezone) | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTime<'format', 'zone', 'deserializeFormats'> | PHP's DateTime object (custom format/timezone, | +| | deserialize format). If you do not want to | +| | specify a specific timezone, use an empty | +| | string (''). DeserializeFormats can either be a | +| | string or an array of string. | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTimeImmutable | PHP's DateTimeImmutable object (default format*/ | +| | timezone). | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTimeImmutable<'format'> | PHP's DateTimeImmutable object (custom format/ | +| | default timezone) | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTimeImmutable<'format', 'zone'> | PHP's DateTimeImmutable object (custom format/ | +| | timezone) | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTimeImmutable<'format', 'zone', 'deserializeFormats'> | PHP's DateTimeImmutable object (custom format/ | +| | timezone/deserialize format). If you do not want | +| | to specify a specific timezone, use an empty | +| | string (''). DeserializeFormats can either be a | +| | string or an array of string. | ++------------------------------------------------------------+--------------------------------------------------+ +| DateInterval | PHP's DateInterval object using ISO 8601 format | ++------------------------------------------------------------+--------------------------------------------------+ +| T | Where T is a fully qualified class name. | ++------------------------------------------------------------+--------------------------------------------------+ +| iterable | Similar to array. Will always be deserialized | +| | into array as implementation info is lost during | +| | serialization. | ++------------------------------------------------------------+--------------------------------------------------+ +| iterable | Similar to array. Will always be deserialized | +| | into array as implementation info is lost during | +| | serialization. | ++------------------------------------------------------------+--------------------------------------------------+ +| iterable | Similar to array. Will always be | +| | deserialized into array as implementation info | +| | is lost during serialization. | ++------------------------------------------------------------+--------------------------------------------------+ +| ArrayCollection | Similar to array, but will be deserialized | +| | into Doctrine's ArrayCollection class. | ++------------------------------------------------------------+--------------------------------------------------+ +| ArrayCollection | Similar to array, but will be deserialized | +| | into Doctrine's ArrayCollection class. | ++------------------------------------------------------------+--------------------------------------------------+ +| Generator | Similar to array, but will be deserialized | +| | into Generator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| Generator | Similar to array, but will be deserialized | +| | into Generator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| Generator | Similar to array, but will be deserialized | +| | into Generator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| ArrayIterator | Similar to array, but will be deserialized | +| | into ArrayIterator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| ArrayIterator | Similar to array, but will be deserialized | +| | into ArrayIterator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| ArrayIterator | Similar to array, but will be deserialized | +| | into ArrayIterator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| Iterator | Similar to array, but will be deserialized | +| | into ArrayIterator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| Iterator | Similar to array, but will be deserialized | +| | into ArrayIterator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| Iterator | Similar to array, but will be deserialized | +| | into ArrayIterator class. | ++------------------------------------------------------------+--------------------------------------------------+ (*) If the standalone jms/serializer is used then default format is `\DateTime::ISO8601` (which is not compatible with ISO-8601 despite the name). For jms/serializer-bundle the default format is `\DateTime::ATOM` (the real ISO-8601 format) but it can be changed in `configuration`_. @@ -463,6 +465,11 @@ Examples: */ private $endAt; + /** + * @Type("DateTime<'Y-m-d', '', ['Y-m-d', 'Y/m/d']>") + */ + private $publishedAt; + /** * @Type("DateTimeImmutable") */ @@ -473,6 +480,11 @@ Examples: */ private $updatedAt; + /** + * @Type("DateTimeImmutable<'Y-m-d', '', ['Y-m-d', 'Y/m/d']>") + */ + private $deletedAt; + /** * @Type("boolean") */ diff --git a/src/Handler/DateHandler.php b/src/Handler/DateHandler.php index e20e04a7c..a004dc15b 100644 --- a/src/Handler/DateHandler.php +++ b/src/Handler/DateHandler.php @@ -221,23 +221,32 @@ public function deserializeDateIntervalFromJson(DeserializationVisitorInterface private function parseDateTime($data, array $type, bool $immutable = false): \DateTimeInterface { $timezone = !empty($type['params'][1]) ? new \DateTimeZone($type['params'][1]) : $this->defaultTimezone; - $format = $this->getDeserializationFormat($type); + $formats = $this->getDeserializationFormats($type); + + $formatTried = []; + foreach ($formats as $format) { + if ($immutable) { + $datetime = \DateTimeImmutable::createFromFormat($format, (string) $data, $timezone); + } else { + $datetime = \DateTime::createFromFormat($format, (string) $data, $timezone); + } - if ($immutable) { - $datetime = \DateTimeImmutable::createFromFormat($format, (string) $data, $timezone); - } else { - $datetime = \DateTime::createFromFormat($format, (string) $data, $timezone); - } + if (false !== $datetime) { + if ('U' === $format) { + $datetime = $datetime->setTimezone($timezone); + } - if (false === $datetime) { - throw new RuntimeException(sprintf('Invalid datetime "%s", expected format %s.', $data, $format)); - } + return $datetime; + } - if ('U' === $format) { - $datetime = $datetime->setTimezone($timezone); + $formatTried[] = $format; } - return $datetime; + throw new RuntimeException(sprintf( + 'Invalid datetime "%s", expected one of the format %s.', + $data, + '"' . implode('", "', $formatTried) . '"' + )); } private function parseDateInterval(string $data): \DateInterval @@ -255,15 +264,13 @@ private function parseDateInterval(string $data): \DateInterval /** * @param array $type */ - private function getDeserializationFormat(array $type): string + private function getDeserializationFormats(array $type): array { if (isset($type['params'][2])) { - return $type['params'][2]; - } - if (isset($type['params'][0])) { - return $type['params'][0]; + return is_array($type['params'][2]) ? $type['params'][2] : [$type['params'][2]]; } - return $this->defaultFormat; + + return [$this->getFormat($type)]; } /** diff --git a/src/Type/InnerParser.php b/src/Type/InnerParser.php index eb05aa448..736f52e42 100644 --- a/src/Type/InnerParser.php +++ b/src/Type/InnerParser.php @@ -23,6 +23,8 @@ public function __construct() [ 'default' => [ 'skip' => '\s+', + 'array_' => '\[', + '_array' => '\]', 'parenthesis_' => '<', '_parenthesis' => '>', 'empty_string' => '""|\'\'', @@ -60,22 +62,33 @@ public function __construct() 14 => new Token(14, 'apostrophed_string', null, -1, true), 15 => new Token(15, '_apostrophe', null, -1, false), 16 => new Concatenation(16, [13, 14, 15], '#simple_type'), - 'simple_type' => new Choice('simple_type', [2, 4, 6, 8, 12, 16], null), - 18 => new Token(18, 'name', null, -1, true), - 19 => new Token(19, 'parenthesis_', null, -1, false), - 20 => new Token(20, 'comma', null, -1, false), - 21 => new Concatenation(21, [20, 'type'], '#compound_type'), - 22 => new Repetition(22, 0, -1, 21, null), - 23 => new Token(23, '_parenthesis', null, -1, false), - 'compound_type' => new Concatenation('compound_type', [18, 19, 'type', 22, 23], null), + 17 => new Concatenation(17, ['array'], '#simple_type'), + 'simple_type' => new Choice('simple_type', [2, 4, 6, 8, 12, 16, 17], null), + 19 => new Token(19, 'name', null, -1, true), + 20 => new Token(20, 'parenthesis_', null, -1, false), + 21 => new Token(21, 'comma', null, -1, false), + 22 => new Concatenation(22, [21, 'type'], '#compound_type'), + 23 => new Repetition(23, 0, -1, 22, null), + 24 => new Token(24, '_parenthesis', null, -1, false), + 'compound_type' => new Concatenation('compound_type', [19, 20, 'type', 23, 24], null), + 26 => new Token(26, 'array_', null, -1, false), + 27 => new Token(27, 'comma', null, -1, false), + 28 => new Concatenation(28, [27, 'simple_type'], '#array'), + 29 => new Repetition(29, 0, -1, 28, null), + 30 => new Concatenation(30, ['simple_type', 29], null), + 31 => new Repetition(31, 0, 1, 30, null), + 32 => new Token(32, '_array', null, -1, false), + 'array' => new Concatenation('array', [26, 31, 32], null), ], [] ); $this->getRule('type')->setPPRepresentation(' simple_type() | compound_type()'); $this->getRule('simple_type')->setDefaultId('#simple_type'); - $this->getRule('simple_type')->setPPRepresentation(' | | | | ::quote_:: ::_quote:: | ::apostrophe_:: ::_apostrophe::'); + $this->getRule('simple_type')->setPPRepresentation(' | | | | ::quote_:: ::_quote:: | ::apostrophe_:: ::_apostrophe:: | array()'); $this->getRule('compound_type')->setDefaultId('#compound_type'); $this->getRule('compound_type')->setPPRepresentation(' ::parenthesis_:: type() ( ::comma:: type() )* ::_parenthesis::'); + $this->getRule('array')->setDefaultId('#array'); + $this->getRule('array')->setPPRepresentation(' ::array_:: ( simple_type() ( ::comma:: simple_type() )* )? ::_array::'); } } diff --git a/src/Type/TypeVisitor.php b/src/Type/TypeVisitor.php index 6b9257c62..ec140b100 100644 --- a/src/Type/TypeVisitor.php +++ b/src/Type/TypeVisitor.php @@ -22,6 +22,8 @@ public function visit(Element $element, &$handle = null, $eldnah = null) return $this->visitSimpleType($element); case '#compound_type': return $this->visitCompoundType($element, $handle, $eldnah); + case '#array': + return $this->visitArrayType($element, $handle, $eldnah); } throw new InvalidNode(); @@ -33,6 +35,11 @@ public function visit(Element $element, &$handle = null, $eldnah = null) private function visitSimpleType(TreeNode $element) { $tokenNode = $element->getChild(0); + + if (!$tokenNode->isToken()) { + return $tokenNode->accept($this); + } + $token = $tokenNode->getValueToken(); $value = $tokenNode->getValueValue(); @@ -76,4 +83,14 @@ function (TreeNode $node) use ($handle, $eldnah) { ), ]; } + + private function visitArrayType(TreeNode $node, ?int &$handle, ?int $eldnah): array + { + return array_map( + function (TreeNode $child) { + return $child->accept($this); + }, + $node->getChildren() + ); + } } diff --git a/src/Type/grammar.pp b/src/Type/grammar.pp index fe98e784e..2192d6c86 100644 --- a/src/Type/grammar.pp +++ b/src/Type/grammar.pp @@ -1,5 +1,7 @@ %skip whitespace \s+ +%token array_ \[ +%token _array \] %token parenthesis_ < %token _parenthesis > %token empty_string ""|'' @@ -26,6 +28,7 @@ | | ::quote_:: ::_quote:: | ::apostrophe_:: ::_apostrophe:: + | array() #compound_type: @@ -33,3 +36,6 @@ type() ( ::comma:: type() )* ::_parenthesis:: + +#array: + ::array_:: ( simple_type() ( ::comma:: simple_type() )* )? ::_array:: diff --git a/tests/Handler/DateHandlerTest.php b/tests/Handler/DateHandlerTest.php index 224ece1a5..881c664d2 100644 --- a/tests/Handler/DateHandlerTest.php +++ b/tests/Handler/DateHandlerTest.php @@ -33,6 +33,7 @@ public function getParams() [['Y-m-d']], [['Y-m-d', '', 'Y-m-d|']], [['Y-m-d', '', 'Y']], + [['Y-m-d', '', ['Y-m-d', 'Y/m/d']]], ]; } @@ -65,6 +66,17 @@ public function testTimePartGetsRemoved() ); } + public function testMultiFormatCase() + { + $visitor = new JsonDeserializationVisitor(); + + $type = ['name' => 'DateTime', 'params' => ['Y-m-d', '', ['Y-m-d|', 'Y/m/d']]]; + self::assertEquals( + \DateTime::createFromFormat('Y/m/d', '2017/06/18', $this->timezone), + $this->handler->deserializeDateTimeFromJson($visitor, '2017/06/18', $type) + ); + } + public function testTimePartGetsPreserved() { $visitor = new JsonDeserializationVisitor(); diff --git a/tests/Serializer/BaseSerializationTest.php b/tests/Serializer/BaseSerializationTest.php index dc02723a6..a343e8b75 100644 --- a/tests/Serializer/BaseSerializationTest.php +++ b/tests/Serializer/BaseSerializationTest.php @@ -665,6 +665,7 @@ public function getDateTime() { return [ ['date_time', new \DateTime('2011-08-30 00:00', new \DateTimeZone('UTC')), 'DateTime'], + ['date_time_multi_format', new \DateTime('2011-08-30 00:00', new \DateTimeZone('UTC')), "DateTime<'Y-m-d', '', ['Y-m-d','Y-m-d\TH:i:sP']>"], ]; } diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index 471d5e378..09f88ddfe 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -93,6 +93,7 @@ protected function getContent($key) $outputs['object_when_null_and_serialized'] = '{"author":null,"text":"foo"}'; $outputs['date_time'] = '"2011-08-30T00:00:00+00:00"'; $outputs['date_time_immutable'] = '"2011-08-30T00:00:00+00:00"'; + $outputs['date_time_multi_format'] = '"2011-08-30T00:00:00+00:00"'; $outputs['timestamp'] = '{"timestamp":1455148800}'; $outputs['timestamp_prev'] = '{"timestamp":"1455148800"}'; $outputs['date_interval'] = '"PT45M"'; diff --git a/tests/Serializer/Type/ParserTest.php b/tests/Serializer/Type/ParserTest.php index 9ceec0d6d..cab16096e 100644 --- a/tests/Serializer/Type/ParserTest.php +++ b/tests/Serializer/Type/ParserTest.php @@ -96,6 +96,30 @@ public function validTypesProvider(): iterable 'Foo<"asdf asdf">', $type('Foo', ['asdf asdf']), ]; + yield [ + 'Foo<[]>', + $type('Foo', [[]]), + ]; + yield [ + 'Foo<[[]]>', + $type('Foo', [[[]]]), + ]; + yield [ + 'Foo<[123]>', + $type('Foo', [[123]]), + ]; + yield [ + 'Foo<[123, 456]>', + $type('Foo', [[123, 456]]), + ]; + yield [ + 'Foo<[[123], 456, "bar"]>', + $type('Foo', [[[123], 456, 'bar']]), + ]; + yield [ + 'DateTime', + $type('DateTime', [null, null, ['Y-m-d\TH:i:s', 'Y-m-d\TH:i:sP']]), + ]; } public function testEmptyString(): void diff --git a/tests/Serializer/xml/date_time_multi_format.xml b/tests/Serializer/xml/date_time_multi_format.xml new file mode 100644 index 000000000..5d0767627 --- /dev/null +++ b/tests/Serializer/xml/date_time_multi_format.xml @@ -0,0 +1,2 @@ + + diff --git a/tests/Serializer/xml/date_time_multi_format_no_cdata.xml b/tests/Serializer/xml/date_time_multi_format_no_cdata.xml new file mode 100644 index 000000000..d596a0f83 --- /dev/null +++ b/tests/Serializer/xml/date_time_multi_format_no_cdata.xml @@ -0,0 +1,2 @@ + +2011-08-30T00:00:00+00:00