api-platform / core

The server component of API Platform: hypermedia and GraphQL APIs in minutes
https://api-platform.com
MIT License
2.39k stars 847 forks source link

DateTime validation not working #1157

Closed Padam87 closed 6 years ago

Padam87 commented 7 years ago
    /**
     * @var \DateTime
     *
     * @ORM\Column(name="from_time", type="datetime")
     * @Assert\NotBlank()
     * @Assert\DateTime()
     */
    protected $from;

When posting null: NotBlank is ignored, actual date is used. I presume it is interpreted as new \DateTime(null)

When posting a non-datetime string: DateTime::__construct(): Failed to parse time string (test) at position 0 (t): The timezone could not be found in the database

dunglas commented 7 years ago

Can you post the full code of your entity please?

Padam87 commented 7 years ago

It is just a test project, so sure: https://gist.github.com/Padam87/25a331c348f5fe5f3b9ec21d4ffa55fe

I'm using "api-platform/core": "2.1.x-dev", because I needed the oauth support.

jacquesndl commented 6 years ago

Hello guys,

I have exactly the same error describe by @Padam87 . You can find the full code of my entity:

<?php

namespace AppBundle\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Post
 *
 * @ORM\Table(name="post")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\PostRepository")
 * @ApiResource()
 */
class Post
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="title", type="string", length=255)
     * @Assert\NotBlank()
     */
    private $title;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="date", type="datetimetz")
     * @Assert\NotBlank()
     * @Assert\Date()
     */
    private $date;

    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set title
     *
     * @param string $title
     *
     * @return Post
     */
    public function setTitle($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * Get title
     *
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * Set date
     *
     * @param \DateTime $date
     *
     * @return Post
     */
    public function setDate(\DateTime $date)
    {
        $this->date = $date;

        return $this;
    }

    /**
     * Get date
     *
     * @return \DateTime
     */
    public function getDate()
    {
        return $this->date;
    }
}

Thanks

sh41 commented 6 years ago

To prevent today's date from being used when a null is posted for a \DateTime type add a datetime_format to your denormalization context.

<?php

namespace AppBundle\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table
 * @ORM\Entity
 * @ApiResource(
 *     itemOperations={
 *         "put"={"denormalization_context"={"datetime_format"="Y-m-d\TH:i:sP"}
 *     },
 *     collectionOperations={
 *         "post"={"denormalization_context"={"datetime_format"="Y-m-d\TH:i:sP"}
 *     }
 * )
 */
class dummyObject
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(type="datetime")
     */
    private $dummyDate;

}

The format Y-m-d\TH:i:sP is ISO8601 - for some reason c will not work here - this is a limitation of the underlying \DateTime::createFromFormat method. I also couldn't find a way to make the \DateTime::ATOM constants work. ISO8601 is the format that \DateTime is serialized to, if you are serialising dates or accepting them back in a different format you should match that format here.

This won't give you a nice friendly validation failure message, but it will at least stop nulls from being interpreted as now. This works by telling the DateTimeNormalizer which format to use instead of letting it accept anything that \DateTime::_construct will accept as valid, which includes null and any of the valid Date and Time Formats.

For a more complete fix there is some more thought required. All \DateTime types appear to be automatically denormalized from a string to a \DateTime type by the DateTimeNormalizer long before the validators get a look at the data. This makes any @Assert\Date() or @Assert\NotBlank on a \DateTime field useless.

piotrbrzezina commented 6 years ago

maybe better idea is to decorate DateTimeNomalizer


class DateTimeNullNormalizer implements NormalizerInterface, DenormalizerInterface
{
    private $dateTimeNormalizer;

    public function __construct(DateTimeNormalizer $dateTimeNormalizer)
    {
        $this->dateTimeNormalizer = $dateTimeNormalizer;
    }

    public function supportsDenormalization($data, $type, $format = null)
    {
        return $this->dateTimeNormalizer->supportsDenormalization($data, $type, $format);
    }

    public function supportsNormalization($data, $format = null)
    {
        return $this->dateTimeNormalizer->supportsNormalization($data, $format);
    }

    public function denormalize($data, $class, $format = null, array $context = array())
    {
        if ($data == null) {
            return $data;
        }

        return $this->dateTimeNormalizer->denormalize($data, $class, $format, $context);
    }

    public function normalize($object, $format = null, array $context = array())
    {
        return $this->dateTimeNormalizer->normalize($object, $format, $context);
    }
}
  AppBundle\Serializer\DateTimeNullNormalizer:
            class:     AppBundle\Serializer\DateTimeNullNormalizer
            decorates: serializer.normalizer.datetime
            arguments: ['@AppBundle\Serializer\DateTimeNullNormalizer.inner']

This way we have nice errors from the validator

Kleinast commented 6 years ago

Bonjour,

Nous avons eu le même problème merci @piotrbrzezina pour ta solution. Ne pas oublier non plus de modifier le setter: public function setDate(\DateTime $date) en public function setDate(\DateTime $date = null)

Pour que le code aille jusqu'aux validateurs.

erikkubica commented 4 years ago

Similar issue here (after 3 years),

/**
 * @ORM\Column(type="datetime", nullable=false)
 * @Groups({"schedule.read","schedule.list"})
 * @Assert\NotBlank(message="field.not_blank")
 * @Assert\DateTime(message="field.invalid_date")
 */

I submit empty date (empty string or null) even after implementing answer from @piotrbrzezina it throws: Failed to denormalize attribute "date" value for class "App\Entity\Schedule": Expected argument of type "DateTimeInterface", "string" given at property path "date".

mbrodala commented 2 years ago

For anyone who ends up here here's a shortcut to a groundwork change in the Symfony Serializer component which will be released with 5.4/6.0:

https://github.com/symfony/symfony/pull/42502

Until then an interim fix is making the property nullable, thus e.g. ?\DateTimeImmutable instead of \DateTimeImmutable.

This works fine together with nullable=false (which is the default anyways) of @ORM\Column but still allows empty/null values at runtime to let the NotBlank assertion to do its work.