array:3 [ "date" => "2024-01-09 17:04:37.209330" "timezone_type" => 3 "timezon" /> array:3 [ "date" => "2024-01-09 17:04:37.209330" "timezone_type" => 3 "timezon" /> array:3 [ "date" => "2024-01-09 17:04:37.209330" "timezone_type" => 3 "timezon"/>

How to denormalize a DateTime-Array to DateTime-Object with Symfony Serializer

276 views Asked by At

I need to denormalize this data with symfony 6

array:4 [
  "createdAt" => array:3 [
    "date" => "2024-01-09 17:04:37.209330"
    "timezone_type" => 3
    "timezone" => "Europe/Paris"
  ]
  "utilisateur" => "phpunit"
  "type" => "CREATION"
  "texte" => "creation de la demande"
]

to this object

class Historique
{
    public \DateTime $createdAt;

    public function __construct(public readonly string $utilisateur, public readonly string $type, public readonly ?string $texte)
    {
        $this->createdAt = new \DateTime();
    }

    public function getTypeLabel(): string
    {
        return HistoriqueTypeEnum::getLabelName($this->type);
    }
}

I have use this code, but I have a problem to denormalize the DateTime object.

$normalizers = [
    new DateTimeNormalizer(),
    new ObjectNormalizer(null, null, null, new ReflectionExtractor()),
    new BackedEnumNormalizer(),
];
$serializer = new Serializer($normalizers, [new JsonEncoder()]);
$serializer->denormalize($historique, Historique::class, 'json', [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i:s',]);

I get this error:

The data is either not an string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.

If I change the order of normalizers like this:

$normalizers = [
    new ObjectNormalizer(null, null, null, new ReflectionExtractor()),
    new BackedEnumNormalizer(),
    new DateTimeNormalizer(),
];

I get this error:

Cannot create an instance of "DateTimeZone" from serialized data because its constructor requires the following parameters to be present : "$timezone".")

2

There are 2 answers

2
SubCore On BEST ANSWER

The DateTimeNormalizer class needs a valid DateTime string. Instead to ignore the DateTime object, you can change the data array and set the createdAt key to the date string and set the timezone with DateTimeNormalizer::TIMEZONE_KEY

$historique = [
  "createdAt" => [
    "date" => "2024-01-09 17:04:37.209330",
    "timezone_type" => 3,
    "timezone" => "Europe/Paris"
  ],
  "utilisateur" => "phpunit",
  "type" => "CREATION",
  "texte" => "creation de la demande",
];

$createdAt = $historique['createdAt'];
$historique['createdAt'] = $createdAt['date'];

$normalizers = [
    new DateTimeNormalizer(),
    new ObjectNormalizer(null, null, null, new ReflectionExtractor()),
    new BackedEnumNormalizer(),
];
$serializer = new Serializer($normalizers, [new JsonEncoder()]);
$object = $serializer->denormalize(
    $historique, 
    Historique::class, 
    'json', 
    [
        DateTimeNormalizer::TIMEZONE_KEY => $createdAt['timezone'],
    ]
);
dump($object);

// Result:
// ^ Historique^ {#114
//   +createdAt: DateTime @1704816277 {#104
//     date: 2024-01-09 17:04:37.209330 Europe/Paris (+01:00)
//   }
//   +utilisateur: "phpunit"
//   +type: "CREATION"
//   +texte: "creation de la demande"
// }
2
Bademeister On

You need a separate denormaliser for your requirement if your example array is the input.

MyDateTimeType

To recognise the property, create your own type. This type inherits \DateTime.

namespace App\Normalizer;

class MyDateTimeType extends \DateTime { }

Historique

class Historique
{
    public MyDateTimeType $createdAt;

    public function __construct(public readonly string $utilisateur, public readonly string $type, public readonly ?string $texte)
    {
        $this->createdAt = new MyDateTimeType();
    }

    public function getTypeLabel(): string
    {
        return $this->type;
    }
}

MyDateTimeTypeDenormalizer

The DateTimeNormaliser is inherited for the normalize process.

namespace App\Normalizer;

use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

class MyDateTimeTypeDenormalizer extends DateTimeNormalizer implements DenormalizerInterface
{
    public function denormalize(mixed $data, string $type, string $format = null, array $context = []): \DateTimeInterface
    {
        return new MyDateTimeType($data['date'], new \DateTimeZone($data['timezone']));
    }

    public function supportsDenormalization(mixed $data, string $type, string $format = null): bool
    {
        return MyDateTimeType::class === $type;
    }
}

Example + Test

use App\Normalizer\Historique;
use App\Normalizer\MyDateTimeTypeDenormalizer;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

class HistoriqueTest extends KernelTestCase
{
    public function testHistorique()
    {
        $historique = [
            "createdAt" => [
                "date" => "2024-01-09 17:04:37.209330",
                "timezone_type" => 3,
                "timezone" => "Europe/Paris"
            ],
            "utilisateur" => "phpunit",
            "type" => "CREATION",
            "texte" => "creation de la demande"
        ];

        $normalizers = [
            new MyDateTimeTypeDenormalizer(),
            new ObjectNormalizer(
                new ClassMetadataFactory(new AnnotationLoader(null)),
                null,
                null,
                new ReflectionExtractor()
            ),
            new BackedEnumNormalizer()
        ];

        $serializer = new Serializer($normalizers, [new JsonEncoder()]);

        $result = $serializer->denormalize(
           $historique,
           Historique::class,
           'json',
           [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i:s']
        );

        $this->assertSame('2024-01-09T17:04:37+01:00', $result->createdAt->format('c'));
    }
}