Search code examples
phpsymfonyxml-deserializationjmsserializerbundlejms-serializer

JMSSerializer: Deserializing empty DateTime XML element into PHP "null" object


I'm working on the deserialization of a XML file. It's possible that some elements won't contain any data so I'm trying to deserialize the following XML element (OfferDate) into a null object instead of a \DateTime object:

<Product>
    <OfferDate></OfferDate>
</Product>

... but I'm getting the following error:

JMS\Serializer\Exception\RuntimeException: Invalid datetime "", expected format Y-m-d\TH:i:s.

./vendor/jms/serializer/src/JMS/Serializer/Handler/DateHandler.php:117
./vendor/jms/serializer/src/JMS/Serializer/Handler/DateHandler.php:99
./vendor/jms/serializer/src/JMS/Serializer/GraphNavigator.php:180
./vendor/jms/serializer/src/JMS/Serializer/XmlDeserializationVisitor.php:280
./vendor/jms/serializer/src/JMS/Serializer/GraphNavigator.php:236
./vendor/jms/serializer/src/JMS/Serializer/XmlDeserializationVisitor.php:175
./vendor/jms/serializer/src/JMS/Serializer/GraphNavigator.php:130
./vendor/jms/serializer/src/JMS/Serializer/XmlDeserializationVisitor.php:251
./vendor/jms/serializer/src/JMS/Serializer/GraphNavigator.php:236
./vendor/jms/serializer/src/JMS/Serializer/Serializer.php:182
./vendor/jms/serializer/src/JMS/Serializer/Serializer.php:116
./vendor/phpoption/phpoption/src/PhpOption/Some.php:89
./vendor/jms/serializer/src/JMS/Serializer/Serializer.php:119
./tests/AppBundle/Domain/Model/ProductTest.php:35
./tests/AppBundle/Domain/Model/ProductTest.php:44

If the XML file would contain for example 2016-09-25T18:58:55 in OfferDate it would work, since there is some data ... But since it's also possible that there can be elements without any data I have to involve this case too.

My YML mapping to deserialize the XML into a object:

AppBundle\Domain\Model\Product:
  xml_root_name: Product
  properties:
    offerDate:
      serialized_name: OfferDate
      type: DateTime<'Y-m-d\TH:i:s'>

My Product class:

<?php
declare(strict_types = 1);

namespace AppBundle\Domain\Model;

/**
 * @author ...
 */
class Product
{

    /**
     * @var \DateTime
     */
    private $offerDate;

    /**
     * @return \DateTime
     */
    public function getOfferDate(): \DateTime
    {
        return $this->offerDate;
    }

}

And finally my deserialization:

$xml = file_get_contents(__DIR__.'/product.xml');

$serializer = SerializerBuilder::create()
                               ->addMetadataDir(__DIR__.'/../../../../app/config/serializer')
                               ->build();

/** @var ProductCollection $productCollection */
$productCollection  = $serializer->deserialize($xml, ProductCollection::class, 'xml');
$firstProduct = $productCollection->getProducts()[0];

var_dump($firstProduct->getOfferDate());

./tests/AppBundle/Domain/Model/ProductTest.php:35 as seen above in the error equals the line $productCollection = $serializer->deserialize($xml, ProductCollection::class, 'xml');.

To clarify why I deserialize into a ProductCollection: The product.xml contains a <Products> element in which <Product> elements are. The ProductCollection then contains a method called getProducts() which returns an array containing the deserialized Product objects.

Is there a way to deserialize the OfferDate element, without any data, into a null object? And if so, how?


Solution

  • I've came up with creating a Handler for the deserialization process of DateTime objects.

    Here's my solution. My DateTimeHandler overriding the default DateHandlers class and method deserializeDateTimeFromXml provided by the JMSSerializer:

    <?php
    declare(strict_types = 1);
    
    namespace AppBundle\Serializer\Handler;
    
    use JMS\Serializer\Handler\DateHandler;
    use JMS\Serializer\XmlDeserializationVisitor;
    
    /**
     * @author ...
     */
    class DateTimeHandler extends DateHandler
    {
        /**
         * @param XmlDeserializationVisitor $visitor
         * @param $data
         * @param array $type
         *
         * @return \DateTime|null
         */
        public function deserializeDateTimeFromXml(XmlDeserializationVisitor $visitor, $data, array $type)
        {
            // Casting the data to a string will return the value of the
            // current xml element. So if it's empty there is no data.
            if ((string)$data === '') {
                return null;
            }
    
            return parent::deserializeDateTimeFromXml($visitor, $data, $type);
        }
    }
    

    Then in my deserialization: (notice the configureHandlers method)

    $xml = file_get_contents(__DIR__.'/product.xml');
    
    $serializer = SerializerBuilder::create()
                                   ->addMetadataDir(__DIR__.'/../../../../app/config/serializer')
                                   ->configureHandlers(
                                       function (HandlerRegistry $registry) {
                                           $registry->registerSubscribingHandler(new DateTimeHandler());
                                       }
                                   )
                                   ->build();
    
    /** @var ProductCollection $productCollection */
    $productCollection = $serializer->deserialize($xml, ProductCollection::class, 'xml');
    

    This now works perfectly fine!