From 97ec3b552bd9a9bcf58ab6c2eaabfccd80502b23 Mon Sep 17 00:00:00 2001 From: Anthon Pang Date: Thu, 8 Nov 2012 17:40:37 +0000 Subject: [PATCH 1/4] Add InitializedObjectConstructor as a test double for DoctrineObjectConstructor. Add tests to BaseSerializationTest: * testNull when expecting types * testNumerics when expecting numeric types * testDateTime * testDeserializingNull when using InitializedObjectConstructor Add content for blog_post_unauthored and date_time: * JsonSerializationTest * xml/blog_post_unauthored.xml * xml/date_time.xml * yml/blog_post_unauthored.yml * yml/date_time.yml So, from the phpunit test results, we see: * fails when serializing a simple DateTime * error when deserialing a null when expecting a DateTime * error: no support for the "float" alias for "double" * error when deserializing: a null value will not overwrite a non-null property in the initialized object --- .../Fixtures/InitializedObjectConstructor.php | 45 ++++++++++ Tests/Serializer/BaseSerializationTest.php | 83 +++++++++++++++++-- Tests/Serializer/JsonSerializationTest.php | 15 ++-- Tests/Serializer/xml/blog_post_unauthored.xml | 5 ++ Tests/Serializer/xml/date_time.xml | 2 + Tests/Serializer/yml/blog_post_unauthored.yml | 4 + Tests/Serializer/yml/date_time.yml | 1 + 7 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 Tests/Fixtures/InitializedObjectConstructor.php create mode 100644 Tests/Serializer/xml/blog_post_unauthored.xml create mode 100644 Tests/Serializer/xml/date_time.xml create mode 100644 Tests/Serializer/yml/blog_post_unauthored.yml create mode 100644 Tests/Serializer/yml/date_time.yml diff --git a/Tests/Fixtures/InitializedObjectConstructor.php b/Tests/Fixtures/InitializedObjectConstructor.php new file mode 100644 index 00000000..3f4d1f56 --- /dev/null +++ b/Tests/Fixtures/InitializedObjectConstructor.php @@ -0,0 +1,45 @@ + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace JMS\SerializerBundle\Tests\Fixtures; + +use Doctrine\Common\Collections\ArrayCollection; + +use JMS\SerializerBundle\Metadata\ClassMetadata; + +use JMS\SerializerBundle\Serializer\Construction\ObjectConstructorInterface; +use JMS\SerializerBundle\Serializer\VisitorInterface; + +use JMS\SerializerBundle\Tests\Fixtures\Author; + +class InitializedObjectConstructor implements ObjectConstructorInterface +{ + public function construct(VisitorInterface $visitor, ClassMetadata $metadata, $data, array $type) + { + $post = new \StdClass; + $post->title = 'This is a nice title.'; + $post->author = new Author('Foo Bar'); + $post->createdAt = new \DateTime('2011-07-30 00:00', new \DateTimeZone('UTC')); + $post->comments = new ArrayCollection(); + $post->published = false; + + $post->comments->add(new \StdClass); + + return $post; + } +} diff --git a/Tests/Serializer/BaseSerializationTest.php b/Tests/Serializer/BaseSerializationTest.php index a4387e4a..87ea4c15 100644 --- a/Tests/Serializer/BaseSerializationTest.php +++ b/Tests/Serializer/BaseSerializationTest.php @@ -60,6 +60,7 @@ use JMS\SerializerBundle\Tests\Fixtures\GroupsObject; use JMS\SerializerBundle\Tests\Fixtures\IndexedCommentsBlogPost; use JMS\SerializerBundle\Tests\Fixtures\InlineParent; +use JMS\SerializerBundle\Tests\Fixtures\InitializedObjectConstructor; use JMS\SerializerBundle\Tests\Fixtures\Log; use JMS\SerializerBundle\Tests\Fixtures\ObjectWithLifecycleCallbacks; use JMS\SerializerBundle\Tests\Fixtures\ObjectWithVersionedVirtualProperties; @@ -106,15 +107,30 @@ public function testSerializeNullObject() $this->serializer->setSerializeNull(false); } - public function testNull() + /** + * @dataProvider getTypes + */ + public function testNull($type) { - $this->assertEquals($this->getContent('null'), $this->serialize(null)); + $this->assertEquals($this->getContent('null'), $this->serialize(null), $type); if ($this->hasDeserializer()) { - $this->assertEquals(null, $this->deserialize($this->getContent('null'), 'NULL')); + $this->assertEquals(null, $this->deserialize($this->getContent('null'), $type)); } } + public function getTypes() + { + return array( + array('NULL'), + array('integer'), + array('double'), + array('float'), + array('string'), + array('DateTime'), + ); + } + public function testString() { $this->assertEquals($this->getContent('string'), $this->serialize('foo')); @@ -144,21 +160,23 @@ public function getBooleans() /** * @dataProvider getNumerics */ - public function testNumerics($key, $value) + public function testNumerics($key, $value, $type) { $this->assertEquals($this->getContent($key), $this->serialize($value)); if ($this->hasDeserializer()) { - $this->assertEquals($value, $this->deserialize($this->getContent($key), is_double($value) ? 'double' : 'integer')); + $this->assertEquals($value, $this->deserialize($this->getContent($key), $type)); } } public function getNumerics() { return array( - array('integer', 1), - array('float', 4.533), - array('float_trailing_zero', 1.0), + array('integer', 1, 'integer'), + array('float', 4.533, 'double'), + array('float', 4.533, 'float'), + array('float_trailing_zero', 1.0, 'double'), + array('float_trailing_zero', 1.0, 'float'), ); } @@ -226,6 +244,29 @@ public function testArrayMixed() $this->assertEquals($this->getContent('array_mixed'), $this->serialize(array('foo', 1, true, new SimpleObject('foo', 'bar'), array(1, 3, true)))); } + /** + * @dataProvider getDateTime + */ + public function testDateTime($key, $value, $type) + { + $this->assertEquals($this->getContent($key), $this->serialize($value, $type)); + + if ($this->hasDeserializer()) { + $deserialized = $this->deserialize($this->getContent($key), $type); + + $this->assertTrue(is_object($deserialized)); + $this->assertEquals(get_class($value), get_class($deserialized)); + $this->assertEquals($value->getTimestamp(), $deserialized->getTimestamp()); + } + } + + public function getDateTime() + { + return array( + array('date_time', new \DateTime('2011-08-30 00:00', new \DateTimeZone('UTC')), 'DateTime'), + ); + } + public function testBlogPost() { $post = new BlogPost('This is a nice title.', $author = new Author('Foo Bar'), new \DateTime('2011-07-30 00:00', new \DateTimeZone('UTC'))); @@ -243,6 +284,32 @@ public function testBlogPost() } } + public function testDeserializingNull() + { + if (get_class($this) === 'JMS\SerializerBundle\Tests\Serializer\XmlSerializationTest') { + $this->markTestSkipped('Deserializing null not working in XML.'); + } + + $objectConstructor = new InitializedObjectConstructor(); + $this->serializer = new Serializer($this->factory, $this->handlerRegistry, $objectConstructor, $this->dispatcher, null, $this->serializationVisitors, $this->deserializationVisitors); + $this->serializer->setSerializeNull(true); + + $post = new BlogPost('This is a nice title.', $author = new Author('Foo Bar'), new \DateTime('2011-07-30 00:00', new \DateTimeZone('UTC'))); + + $this->setField($post, 'author', null); + + $this->assertEquals($this->getContent('blog_post_unauthored'), $this->serialize($post)); + + if ($this->hasDeserializer()) { + $deserialized = $this->deserialize($this->getContent('blog_post_unauthored'), get_class($post)); + $this->assertEquals('2011-07-30T00:00:00+0000', $this->getField($deserialized, 'createdAt')->format(\DateTime::ISO8601)); + $this->assertAttributeEquals('This is a nice title.', 'title', $deserialized); + $this->assertAttributeSame(false, 'published', $deserialized); + $this->assertAttributeEquals(new ArrayCollection(), 'comments', $deserialized); + $this->assertEquals(null, $this->getField($deserialized, 'author')); + } + } + public function testReadOnly() { $author = new AuthorReadOnly(123, 'Ruud Kamphuis'); diff --git a/Tests/Serializer/JsonSerializationTest.php b/Tests/Serializer/JsonSerializationTest.php index aa0e2e14..76e21519 100644 --- a/Tests/Serializer/JsonSerializationTest.php +++ b/Tests/Serializer/JsonSerializationTest.php @@ -18,21 +18,16 @@ namespace JMS\SerializerBundle\Tests\Serializer; -use JMS\SerializerBundle\Serializer\VisitorInterface; - -use JMS\SerializerBundle\Serializer\GraphNavigator; - -use JMS\SerializerBundle\Serializer\EventDispatcher\EventSubscriberInterface; +use JMS\SerializerBundle\Exception\RuntimeException; use JMS\SerializerBundle\Serializer\EventDispatcher\Event; +use JMS\SerializerBundle\Serializer\EventDispatcher\EventSubscriberInterface; +use JMS\SerializerBundle\Serializer\GraphNavigator; +use JMS\SerializerBundle\Serializer\VisitorInterface; use JMS\SerializerBundle\Tests\Fixtures\Author; - use JMS\SerializerBundle\Tests\Fixtures\AuthorList; -use JMS\SerializerBundle\Exception\RuntimeException; -use JMS\SerializerBundle\Tests\Fixtures\SimpleObject; - class JsonSerializationTest extends BaseSerializationTest { protected function getContent($key) @@ -56,6 +51,7 @@ protected function getContent($key) $outputs['array_objects'] = '[{"foo":"foo","moo":"bar","camel_case":"boo"},{"foo":"baz","moo":"boo","camel_case":"boo"}]'; $outputs['array_mixed'] = '["foo",1,true,{"foo":"foo","moo":"bar","camel_case":"boo"},[1,3,true]]'; $outputs['blog_post'] = '{"title":"This is a nice title.","created_at":"2011-07-30T00:00:00+0000","is_published":false,"comments":[{"author":{"full_name":"Foo Bar"},"text":"foo"}],"author":{"full_name":"Foo Bar"}}'; + $outputs['blog_post_unauthored'] = '{"title":"This is a nice title.","created_at":"2011-07-30T00:00:00+0000","is_published":false,"comments":[],"author":null}'; $outputs['price'] = '{"price":3}'; $outputs['currency_aware_price'] = '{"currency":"EUR","amount":2.34}'; $outputs['order'] = '{"cost":{"price":12.34}}'; @@ -86,6 +82,7 @@ protected function getContent($key) $outputs['simple_object_nullable'] = '{"foo":"foo","moo":"bar","camel_case":"boo","null_property":null}'; $outputs['input'] = '{"attributes":{"type":"text","name":"firstname","value":"Adrien"}}'; $outputs['hash_empty'] = '{"hash":{}}'; + $outputs['date_time'] = '"2011-08-30T00:00:00+0000"'; } if (!isset($outputs[$key])) { diff --git a/Tests/Serializer/xml/blog_post_unauthored.xml b/Tests/Serializer/xml/blog_post_unauthored.xml new file mode 100644 index 00000000..6a07ea08 --- /dev/null +++ b/Tests/Serializer/xml/blog_post_unauthored.xml @@ -0,0 +1,5 @@ + + + <![CDATA[This is a nice title.]]> + + diff --git a/Tests/Serializer/xml/date_time.xml b/Tests/Serializer/xml/date_time.xml new file mode 100644 index 00000000..00156d46 --- /dev/null +++ b/Tests/Serializer/xml/date_time.xml @@ -0,0 +1,2 @@ + + diff --git a/Tests/Serializer/yml/blog_post_unauthored.yml b/Tests/Serializer/yml/blog_post_unauthored.yml new file mode 100644 index 00000000..bf2887b5 --- /dev/null +++ b/Tests/Serializer/yml/blog_post_unauthored.yml @@ -0,0 +1,4 @@ +title: 'This is a nice title.' +created_at: '2011-07-30T00:00:00+0000' +is_published: false +author: null diff --git a/Tests/Serializer/yml/date_time.yml b/Tests/Serializer/yml/date_time.yml new file mode 100644 index 00000000..a8f98ee4 --- /dev/null +++ b/Tests/Serializer/yml/date_time.yml @@ -0,0 +1 @@ +'2011-08-30T00:00:00+0000' From 87c232c208a48a403ef121d5d00ad5a4c8d2717a Mon Sep 17 00:00:00 2001 From: Anthon Pang Date: Thu, 8 Nov 2012 18:04:15 +0000 Subject: [PATCH 2/4] GraphNavigator support for "float" alias. --- Serializer/GraphNavigator.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Serializer/GraphNavigator.php b/Serializer/GraphNavigator.php index 937453d5..128070e3 100644 --- a/Serializer/GraphNavigator.php +++ b/Serializer/GraphNavigator.php @@ -129,6 +129,7 @@ public function accept($data, array $type = null, VisitorInterface $visitor) return $visitor->visitBoolean($data, $type); case 'double': + case 'float': return $visitor->visitDouble($data, $type); case 'array': @@ -240,7 +241,7 @@ public function detachObject($object) { if (null === $object) { throw new InvalidArgumentException('$object cannot be null'); - } else if (!is_object($object)) { + } elseif (!is_object($object)) { throw new InvalidArgumentException(sprintf('Expected an object to detach, given "%s".', gettype($object))); } From cc94941b2249423a487b278ba4336f0550ecb90f Mon Sep 17 00:00:00 2001 From: Anthon Pang Date: Thu, 8 Nov 2012 18:21:46 +0000 Subject: [PATCH 3/4] DateTimeHandler: * fix serialization/deserialization of DateTime * there's no change to parseDateTime() other than removing DOS carriage returns --- Serializer/Handler/DateTimeHandler.php | 48 ++++++++++++++++++-------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/Serializer/Handler/DateTimeHandler.php b/Serializer/Handler/DateTimeHandler.php index a81dcabf..987ccc2f 100644 --- a/Serializer/Handler/DateTimeHandler.php +++ b/Serializer/Handler/DateTimeHandler.php @@ -60,48 +60,66 @@ public function __construct($defaultFormat = \DateTime::ISO8601, $defaultTimezon $this->defaultTimezone = new \DateTimeZone($defaultTimezone); } - public function serializeDateTimeToXml(XmlSerializationVisitor $visitor, \DateTime $date, array $type) + public function serializeDateTimeToXml(XmlSerializationVisitor $visitor, $date, array $type) { - if (null === $visitor->document) { - $visitor->document = $visitor->createDocument(null, null, true); - } - - return $visitor->document->createTextNode($date->format($this->getFormat($type))); + if ($date === null) { + return $visitor->visitNull(null, $type); + } + + return $visitor->visitString($date->format($this->getFormat($type)), $type); } - public function serializeDateTimeToYml(YamlSerializationVisitor $visitor, \DateTime $date, array $type) + public function serializeDateTimeToYml(YamlSerializationVisitor $visitor, $date, array $type) { - return Inline::dump($date->format($this->getFormat($type))); + if ($date === null) { + return $visitor->visitNull(null, $type); + } + + return $visitor->visitString($date->format($this->getFormat($type)), $type); } - public function serializeDateTimeToJson(JsonSerializationVisitor $visitor, \DateTime $date, array $type) + public function serializeDateTimeToJson(JsonSerializationVisitor $visitor, $date, array $type) { - return $date->format($this->getFormat($type)); + if ($date === null) { + return $visitor->visitNull(null, $type); + } + + return $visitor->visitString($date->format($this->getFormat($type)), $type); } public function deserializeDateTimeFromXml(XmlDeserializationVisitor $visitor, $data, array $type) { + $attributes = $data->attributes(); + + if (isset($attributes['nil'][0]) && (string) $attributes['nil'][0] === 'true') { + return null; + } + return $this->parseDateTime($data, $type); } public function deserializeDateTimeFromJson(JsonDeserializationVisitor $visitor, $data, array $type) { + if ($data === null) { + return null; + } + return $this->parseDateTime($data, $type); } private function parseDateTime($data, array $type) { $timezone = isset($type['params'][1]) ? $type['params'][1] : $this->defaultTimezone; - $datetime = \DateTime::createFromFormat($this->getFormat($type), (string) $data, $timezone); - if (false === $datetime) { - throw new RuntimeException(sprintf('Invalid datetime "%s", expected format %s.', $data, $this->defaultFormat)); + $datetime = \DateTime::createFromFormat($this->getFormat($type), (string) $data, $timezone); + if (false === $datetime) { + throw new RuntimeException(sprintf('Invalid datetime "%s", expected format %s.', $data, $this->defaultFormat)); } - return $datetime; + return $datetime; } private function getFormat(array $type) { return isset($type['params'][0]) ? $type['params'][0] : $this->defaultFormat; } -} \ No newline at end of file +} From e1ea08d2df7ddc97bc9e83434895769fb9feec59 Mon Sep 17 00:00:00 2001 From: Anthon Pang Date: Thu, 8 Nov 2012 18:30:48 +0000 Subject: [PATCH 4/4] when deserializing, null values should overwrite properties in the initialized object --- Serializer/GenericDeserializationVisitor.php | 13 +++++++++---- Serializer/GenericSerializationVisitor.php | 5 ++++- Serializer/YamlSerializationVisitor.php | 9 ++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Serializer/GenericDeserializationVisitor.php b/Serializer/GenericDeserializationVisitor.php index 1f151805..07bb8bd6 100644 --- a/Serializer/GenericDeserializationVisitor.php +++ b/Serializer/GenericDeserializationVisitor.php @@ -164,7 +164,7 @@ public function visitProperty(PropertyMetadata $metadata, $data) { $name = $this->namingStrategy->translateName($metadata); - if ( ! isset($data[$name])) { + if ( ! array_key_exists($name, $data)) { return; } @@ -172,9 +172,14 @@ public function visitProperty(PropertyMetadata $metadata, $data) throw new RuntimeException(sprintf('You must define a type for %s::$%s.', $metadata->reflection->class, $metadata->name)); } - $v = $this->navigator->accept($data[$name], $metadata->type, $this); - if (null === $v) { - return; + $v = null; + + if ($data[$name] !== null) { + $v = $this->navigator->accept($data[$name], $metadata->type, $this); + + if (null === $v) { + return; + } } if (null === $metadata->setter) { diff --git a/Serializer/GenericSerializationVisitor.php b/Serializer/GenericSerializationVisitor.php index 19026cbf..9de51ae7 100644 --- a/Serializer/GenericSerializationVisitor.php +++ b/Serializer/GenericSerializationVisitor.php @@ -134,7 +134,10 @@ public function visitProperty(PropertyMetadata $metadata, $data) $v = (null === $metadata->getter ? $metadata->reflection->getValue($data) : $data->{$metadata->getter}()); - $v = $this->navigator->accept($v, $metadata->type, $this); + if ($v !== null) { + $v = $this->navigator->accept($v, $metadata->type, $this); + } + if (null === $v && !$this->shouldSerializeNull()) { return; } diff --git a/Serializer/YamlSerializationVisitor.php b/Serializer/YamlSerializationVisitor.php index 71cba87e..e5bc7dcd 100644 --- a/Serializer/YamlSerializationVisitor.php +++ b/Serializer/YamlSerializationVisitor.php @@ -173,18 +173,21 @@ public function visitProperty(PropertyMetadata $metadata, $data) $count = $this->writer->changeCount; - if (null !== $v = $this->navigator->accept($v, $metadata->type, $this)) { + if ($v === null) { + $this->writer->rtrim(false)->writeln(' null'); + } elseif (null !== $v = $this->navigator->accept($v, $metadata->type, $this)) { $this->writer ->rtrim(false) ->writeln(' '.$v) ; - } else if ($count === $this->writer->changeCount) { + } elseif ($count === $this->writer->changeCount) { $this->writer->revert(); } if (!$metadata->inline) { $this->writer->outdent(); } + $this->revertCurrentMetadata(); } @@ -212,4 +215,4 @@ public function getResult() { return $this->writer->getContent(); } -} \ No newline at end of file +}