Fosrestbundle: Handling datetime on POST

Created on 11 Jul 2014  Â·  10Comments  Â·  Source: FriendsOfSymfony/FOSRestBundle

Hello,

I have a weird error I'm trying to resolve since few days, when I expected it to be managed automaticaly from scratch.
So maybe there is a bug somewhere...

I created an API, and I added a POST function.
I'm using a form and JMS serializer to do that, and for all values it works, except for my DateTime Object.

Maybe it's just a matter of format badly defined, but because the error is just : "This value is not valid" I have still no idea on the date format expected.

My entity :

<?php

namespace Exif\CoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation\Expose;
use Symfony\Component\Validator\Constraints as Assert;
/**
 * Photo
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Exif\CoreBundle\Repository\PhotoRepository")
 */
class Photo
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * 
     * @Expose
     */
    private $id;

    /**
     * @var integer
     *
     * @ORM\Column(name="hash", type="integer")
     * @Assert\NotBlank()
     * @Expose
     */
    private $hash;
    /**
     * @var integer
     *
     * @ORM\Column(name="filesize", type="integer")
     * @Assert\NotBlank()
     * @Expose
     */
    private $filesize;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="datePicture", type="datetime")
     * 
     * @Assert\NotBlank()
     * @Expose
     */
    private $datePicture;

    /**
     * @ORM\ManyToOne(targetEntity="Camera", inversedBy="photos")
     * @ORM\JoinColumn(name="camera_id", referencedColumnName="id")
     * 
     * @Assert\NotBlank()
     * @Expose
     **/
    private $camera;
    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set hash
     *
     * @param string $hash
     * @return Photo
     */
    public function setHash($hash)
    {
        $this->hash = $hash;

        return $this;
    }

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

    /**
     * Set datePicture
     *
     * @param \DateTime $datePicture
     * @return Photo
     */
    public function setDatePicture($datePicture)
    {
        $this->datePicture = $datePicture;

        return $this;
    }

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

    /**
     * Set camera
     *
     * @param \Exif\CoreBundle\Entity\Camera $camera
     * @return Photo
     */
    public function setCamera(\Exif\CoreBundle\Entity\Camera $camera = null)
    {
        $this->camera = $camera;

        return $this;
    }

    /**
     * Get camera
     *
     * @return \Exif\CoreBundle\Entity\Camera 
     */
    public function getCamera()
    {
        return $this->camera;
    }

    /**
     * Set filesize
     *
     * @param integer $filesize
     * @return Photo
     */
    public function setFilesize($filesize)
    {
        $this->filesize = $filesize;

        return $this;
    }

    /**
     * Get filesize
     *
     * @return integer 
     */
    public function getFilesize()
    {
        return $this->filesize;
    }
}

My form :

<?php

namespace Exif\CoreBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class PhotoType extends AbstractType
{
        /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('useremail', 'email', array('mapped' => false))
            ->add('hash')
            ->add('filesize')
            ->add('datePicture', 'datetime', array(
                'description' => 'The date when the picture was taken',
                'format' => 'yyyy/MM/dd HH:mm',
            ))
            ->add('camera')
        ;
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Exif\CoreBundle\Entity\Photo',
            'csrf_protection'   => false,
        ));
    }

    /**
     * @return string
     */
    public function getName()
    {
        return '';
    }
}

And my controler : (look cPostAction and processForm)

<?php

namespace Exif\CoreBundle\Controller;

use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Routing\ClassResourceInterface;
use FOS\RestBundle\Controller\Annotations as Rest;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;

use FOS\RestBundle\View\View;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use FOS\RestBundle\Controller\Annotations\Get;
use Exif\CoreBundle\Entity\Photo;
use Exif\CoreBundle\Form\PhotoType;

class PhotosController extends FOSRestController implements ClassResourceInterface {
    /**
     * Collection get action
     * @var Request $request
     * @return array
     *
     * @Rest\View()
     */
    public function cgetAction(Request $request)
    {
        $em = $this->getDoctrine()->getManager();
        $entities = $em->getRepository('ExifCoreBundle:Photo')->findAll();
        return array(
            'entities' => $entities,
        );
    }
    /**
    * @ApiDoc(
    *   resource = true,
    *   description = "return datas about one photo",
    *   input = "integer",
    *   statusCodes = {
    *     200 = "Returned when successful",
    *     404 = "Returned when no photo had been found"
    *   }
    * )
    * @Rest\View()
    */
    public function getAction($id) {
        $photo = $this->getDoctrine()->getRepository('ExifCoreBundle:Photo')->findOneById($id);
        if (!is_object($photo)) {
            throw $this->createNotFoundException();
        }
        return $photo;
    }
    /**
     * @Get("/photos/{hash}/{filesize}")
     * @Rest\View()
     */
    public function searchAction($hash, $filesize) {
        $photo = $this->getDoctrine()->getRepository('ExifCoreBundle:Photo')->findOneBy(array("hash" => $hash, "filesize" => $filesize));
        if (!is_object($photo)) {
            throw $this->createNotFoundException();
        }
        return $photo;
    }
    /**
     * @Rest\View()
     */
    public function getUsersAction($id) {
        $users = $this->getDoctrine()->getRepository('ExifCoreBundle:User')->findByPhoto($id);
        if (empty($users)) {
            throw $this->createNotFoundException();
        }
        return $users;
    }
    /**
    * Collection post action
    * @ApiDoc
    * @var Request $request
    * @return View|array
    */
    public function cpostAction(Request $request)
    {
        return $this->processForm($request,new Photo());
    }
    private function processForm(Request $request, Photo $photo)
    {
        $statusCode = $photo->getId() ? 201 : 204;

        $form = $this->createForm(new PhotoType(), $photo);
        $form->submit($request);

        if ($form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $em->persist($photo);
            $em->flush();

            return $this->redirectView(
                    $this->generateUrl(
                        'api_get_photo',
                        array('id' => $photo->getId())
                        ),
                    Codes::HTTP_CREATED
                    );
        }

        return array(
            'form' => $form,
        );        
    }

    /**
     * Put action
     * @var Request $request
     * @var integer $id Id of the entity
     * @return View|array
     */
    public function putAction($id) {
        $entity = $this->getEntity($id);
        $form = $this->createForm(new PhotoType(), $entity);
        $form->bind($this->getRequest());

        if ($form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $em->persist($entity);
            $em->flush();

            return $this->view(null, Codes::HTTP_NO_CONTENT);
        }

        return array(
            'form' => $form,
        );
    }

}

My query on this url : http://exif.dev/app_dev.php/api/photos.json

filesize=1024&hash=12345678&useremail=t&datePicture=2013%2F10%2F16%2012%3A00&camera=1

and the response :

{"code":400,"message":"Validation Failed","errors":{"children":{"useremail":[],"hash":[],"filesize":[],"datePicture":{"errors":["This value is not valid."],"children":{"date":{"children":{"year":[],"month":[],"day":[]}},"time":{"children":{"hour":[],"minute":[]}}}},"camera":[]}}}

The problem is in the validation I think, but without being able to define the format of datetime neither in form nor in entity, I have no idea what kind of value is waited.

Is it a bug, or just a dumb thing ?

Best regards,
Pierre

Most helpful comment

A default datetime field expects the following structure:

"datePicture": {
    "date": {
        "year": 2014,
        "month": 11,
        "day": 5
    },
    "time": {
        "hour": 23,
        "minute": 11
    }
}

All 10 comments

Hello.

I managed to do it by replacing in my form the type from datetime to text and by applying a transformer on it.
But I don't think it's the best option.
My APIdoc does not include the format expected now.
I can't reuse my form in a view.

Anyone played with form and DateTime ?

best regards,
Pierre

@pmithrandir I got the same problem...

@ousmaneNdiaye @pmithrandir Me too, same problem

A default datetime field expects the following structure:

"datePicture": {
    "date": {
        "year": 2014,
        "month": 11,
        "day": 5
    },
    "time": {
        "hour": 23,
        "minute": 11
    }
}

thanks :+1:

@ousmaneNdiaye Can you please explain it to me? I don't understand what you mean. Thanks

There seems to be a bug in the format configuration option on the datetime-type. The specified format is simply ignored.

To work around this bug and get this working, you should do one of two things.

You can make your API-client send the DateTime value in the structure provided above by @JohannesKlauss.

If you don't wish to use this format on the client-side, you can specify a custom format in the formBuilder configuration. Change:

->add('datePicture', 'datetime', array(
                'description' => 'The date when the picture was taken',
                'format' => 'yyyy/MM/dd HH:mm',
            ))

into

->add('datePicture', 'datetime', array(
                'description' => 'The date when the picture was taken',
                'widget' => 'single_text',
                'date-format' => 'yyyy/MM/dd',
            ))

This got it working for me. Note that specifying the time-format is not necessary because you are using the default format for this.

Thank @bveldhuis. You're absolute right : 'widget' => 'single_text' is important.

For DATE field
following format expected

birthDate:  {
            "year": 2014,
            "month": 11,
            "day": 5

    }

The format provided by @JohannesKlauss works nice thanks ! However I am facing an issue because I nedd to send my datetime including seconds.

Obviously I tried :

"datePicture": {
    "date": {
        "year": 2014,
        "month": 11,
        "day": 5
    },
    "time": {
        "hour": 23,
        "minute": 11,
        "second": 34
    }
}

But the validation failed :

[...]
  -messageTemplate: "This form should not contain extra fields."
  -parameters: [â–¼
    "{{ extra_fields }}" => ""second""`
[...]
  ]

Is this possible to send seconds ?

Was this page helpful?
0 / 5 - 0 ratings