I try to validate a date (or a datetime) with the validation of a form into Symfony (3.2).
I'm using FOSRestBundle to use the json from request (because i try to develop my personnal API)
But i've try a lot of format:
2017-04-09
17-04-09
for datetime:
2017-04-09 21:12:12
2017-04-09T21:12:12
2017-04-09T21:12:12+01:00
...
But the form is not valid and i get always this error:
This value is not valid
The function of my controller
public function postPlacesAction(Request $request) {
$place = new Place();
$form = $this->createForm(PlaceType::class, $place);
$form->handleRequest($request);
if ($form->isValid()) {
return $this->handleView($this->view(null, Response::HTTP_CREATED));
} else {
return $this->handleView($this->view($form->getErrors(), Response::HTTP_BAD_REQUEST));
}
}
My entity
class Place
{
/**
* #var string
*
* #Assert\NotBlank(message = "The name should not be blank.")
*/
protected $name;
/**
* #var string
*
* #Assert\NotBlank(message = "The address should not be blank.")
*/
protected $address;
/**
* #var date
*
* #Assert\Date()
*/
protected $created;
// ....
// Getter and setter of all var
My entity type
class PlaceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
$builder->add('address');
$builder->add('created');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'MyBundle\Entity\Place',
'csrf_protection' => false
]);
}
}
An example of request (i'm using Postman)
Method: POST
Header: application/json
Body (raw):
{"place":{"name":"name","address":"an address","created":"1997-12-12"}}
I'm not sure that i use the right format, or if i missing anything in my files :/
Could you please switch on the light in my mind!?! :)
Thanks so much for your help.
Fabrice
The problem at created field in your form type. When you add created field using $builder->add('created'); syntax, the default type Symfony\Component\Form\Extension\Core\Type\TextType will be applied and 1997-12-12 input data is a string, not a DateTime instance.
To fix this issue, you should pass DateType in second argument: $builder->add('created', 'Symfony\Component\Form\Extension\Core\Type\DateType');. This form type has a transformer which will transform the input data 1997-12-12 into a DateTime instance.
For more informations about Symfony's form types, have a look at Form Types Reference
Related
I got somme issue with Symfony to convert a DateTime into string. I use a DataTransformer to format my Datetime but in the form, there is an error that say : "This value should be of type string".
Here is my code:
My Entity : Shift.php (only the necessary)
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="datetime")
* #Assert\DateTime(message="La date de début doit être au format DateTime")
*/
private $start_at;
My ShiftType :
$builder
->add('start_at', TextType::class, ['attr' => [ 'class' => 'dateTimePicker']])
->add('end_at', TextType::class, ['attr' => [ 'class' => 'dateTimePicker']])
->add('has_eat')
->add('break_duration')
->add('comment')
;
$builder->get('start_at')->addModelTransformer($this->transformer);
$builder->get('end_at')->addModelTransformer($this->transformer);
And my DataTransformer :
/**
* #param DateTime|null $datetime
* #return string
*/
public function transform($datetime)
{
if ($datetime === null)
{
return '';
}
return $datetime->format('Y-m-d H:i');
}
/**
* #param string $dateString
* #return Datetime|null
*/
public function reverseTransform($dateString)
{
if (!$dateString)
{
throw new TransformationFailedException('Pas de date(string) passé');
return;
}
$date = \Datetime::createFromFormat('Y-m-d H:i', $dateString);
if($date === false){
throw new TransformationFailedException("Le format n'est pas le bon (fonction reverseTransform)" . "$dateString");
}
return $date;
}
As i said, when i want submit the form, there are errors with the form.
It said "This value should be of type string." and it's caused by :
Symfony\Component\Validator\ConstraintViolation {#1107 ▼
root: Symfony\Component\Form\Form {#678 …}
path: "data.start_at"
value: DateTime #1578465000 {#745 ▶}
}
Something weard, when i want to edit a shift, Symfony get the date from the db and transform it into string with no error message. But as i want to save the edit, i got the same issue
Could you help me please ?
Thanks
I have had a similar issue in the past when using $this-> inside a form.
Sometimes $this would not contains the current data. It might explain why it loads at first, but, on submit, it might not be filled properly.
I would suggest creating a custom form type for better reusability, and I know it works very well.
Since you already have your DataTransformer class, you would need to create a new custom form Type. This new form type will extend TextType and use the datatransformer.
For example:
namespace App\Form\Branch\Type;
use App\Form\Branch\DataTransformer\PhoneFormatTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PhoneType extends AbstractType
{
private $tools;
public function __construct()
{
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new PhoneFormatTransformer();
$builder->addModelTransformer($transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array());
}
public function getParent()
{
return TextType::class;
}
public function getBlockPrefix()
{
return 'phone';
}
}
If you services are not autowired, you would need to add a service definition with a specific tag
App\Form\Branch\Type\PhoneType:
tags:
- { name: form.type, alias: phone }
All that is left to do is use your new form type in your form builder:
use App\Form\Branch\Type\PhoneType;
...
$builder
->add('phone', PhoneType::class);
It is a tad more work, but it makes it very easy to reuse, which no doubt, you will have to do everytime a datetime field is needed.
Hope this helps!
Thanks for the answer. I tried tour solution and i wil use it, but it was not the solution to my issue. With your code, i got the same error.
The solution was in the Entity. In the annotations of start_at, I did an #Assert/DateTime and it was a bad use of it. I just delete this line and all is now correct :
/**
* #ORM\Column(type="datetime")
*
*/
private $start_at;
/**
* #ORM\Column(type="datetime", nullable=true)
* #Assert\GreaterThan(propertyPath="start_at", message="La date de fin doit être ultérieure...")
*/
private $end_at;
However, i use your code because it's a lot reusable so thank you for your contribution
I have a Task entity, with two mandatory, non-nullable, fields:
title
dueDatetime
and Form to create task. The form is called by external scripts through POST with application/x-www-form-urlencoded (so no json or anything fancy), so I use standard symfony to handle this.
Problem is I don't control the scripts, and if the script forgot one of the argument, symfony4 will directly throw an exception at the handleRequest step, before I have the time to check if the form is valid or not. Which result in an ugly response 500.
My question: How to avoid that ? The best for me would be to just continue to use "form->isValid()" as before , but if there's an other standard way to handle that, it's okay too.
Note: it would be best if I don't have to put my entity's setter as accepting null values
The exception I got:
Expected argument of type "DateTimeInterface", "NULL" given.
in vendor/symfony/property-acces /PropertyAccessor.php::throwInvalidArgumentException (line 153)
in vendor/symfony/form/Extension/Core/DataMapper/PropertyPathMapper.php->setValue (line 85)
in vendor/symfony/form/Form.php->mapFormsToData (line 622)
in vendor/symfony/form/Extension/HttpFoundation/HttpFoundationRequestHandler.php->submit (line 108)
in vendor/symfony/form/Form.php->handleRequest (line 492)
A curl that reproduce the error :
curl -d 'title=foo' http://127.0.0.1:8080/users/api/tasks
The code :
Entity:
class Task
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="bigint")
*/
private $id;
/**
* #Assert\NotNull()
* #Assert\NotBlank()
* #ORM\Column(type="string", length=500)
*/
private $title;
/**
*
* #ORM\Column(type="datetimetz")
*/
private $dueDatetime;
public function getDueDatetime(): ?\DateTimeInterface
{
return $this->dueDatetime;
}
public function setDueDatetime(\DateTimeInterface $dueDatetime): self
{
$this->dueDatetime = $dueDatetime;
return $this;
}
public function setTitle($title)
{
$this->title = $title;
return $this;
}
public function getTitle()
{
return $this->title;
}
}
Form
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('dueDatetime')
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => Task::class]);
}
}
Controller:
class TaskController extends AbstractController
{
/**
* #Route(
* "/users/api/tasks",
* methods={"POST"},
* name="user_api_create_task"
* )
*/
public function apiCreateTask(Request $request)
{
$task = new Task();;
// the use of createNamed with an empty string is just so that
// the external scripts don't have to know about symfony's convention
$formFactory = $this->container->get('form.factory');
$form = $formFactory->createNamed(
'',
TaskType::class,
$task
);
$form->handleRequest($request); // <-- this throw exception
// but this code should handle this no ?
if (!$form->isSubmitted() || !$form->isValid()) {
return new JsonResponse([], 422);
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($task);
$entityManager->flush();
return new JsonResponse();
}
}
There are at least 2 ways to handle this.
In the two ways you will have to add #Assert\NotNull() to the dueDatetime attribute.
1 - You can try/catch the exception of the handleRequest call.[edit] this one breaks the flow, not good.
2 - You can make nullable the setter setDueDatetime(\DateTimeInterface $dueDatetime = null). If you choose this one, please be sure to always validate your entity before an Insert/Update in DB else you will get an SQL error.
In the two cases it will be handled by the validator isValid() and you will have a nice error in your front end.
You need to allow nullable parameter (with "?") in method setDueDatetime
public function setDueDatetime(?\DateTimeInterface $dueDatetime): self
{
$this->dueDatetime = $dueDatetime;
return $this;
}
Never had this problem before.
Fill the form with a phone, leaving lastname blank
Submit the form (and the validation groups become Default and Create)
The error "Last name is required." is mapped on the wrong $phone field, while should be mappend to $lastName itself property
Can you reproduce the same issue?
$phone property is in the Create validation group, while $phone in Default implicit group:
class User
{
/**
* #Assert\NotBlank(groups={"Create"}, message="Last name is required.")
*
* #var string
*/
protected $lastName;
/**
* #Assert\NotBlank(message="Phone is required.")
*
* #var string
*/
protected $phone;
}
I determine the validation groups based on submitted data:
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('lastName', 'text');
$builder->add('phone', 'text');
$builder->add('submit', 'submit');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'required' => false,
'data_class' => 'Acme\HelloBundle\Entity\User',
'validation_groups' => function (FormInterface $form) {
return null === $form->getData()->getId()
? ['Default', 'Create']
: ['Default', 'Edit'];
}
]);
}
}
Instead of using a compiler pass, you can edit config.yml to set the API to 2.4 :
validation:
enable_annotations: true
api: 2.4 # default is auto which sets API 2.5 BC
When the bug is resolved in 2.5, just remove the api setting and you will get back to 2.5 backward compatible.
Warning there is a bug with validation API 2.5
Took a couple of hours but I found it! Actually is an issue (https://github.com/symfony/symfony/issues/11003) for the new validator API 2.5.
Temporary solution (compiler pass):
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Validator\Validation;
class SetValidatorBuilderApiVersionWorkaround implements CompilerPassInterface
{
/**
* {#inheritDoc}
*/
public function process(ContainerBuilder $container)
{
// TODO remove when https://github.com/symfony/symfony/issues/11003
// is fixed (validation errors added to the wrong field)
$container->getDefinition('validator.builder')
->addMethodCall('setApiVersion', [Validation::API_VERSION_2_4]);
}
}
this is my very first question :S
I'm using Symfony2 and i'm having the following trouble
I have two entities related in a ManyToOne relation, I want to make a form for the followin entity
/**
* #ORM\Entity
* #ORM\Table(name="product")
*/
class Product
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=100)
*/
protected $name;
/**
* #ORM\ManyToOne(targetEntity="Acme\ProductsBundle\Entity\ProductCategory", inversedBy="products")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
protected $productCategory;
}
So i did the following "ProductType"
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('productCategory')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\ProductsBundle\Entity\Product'
));
}
public function getName()
{
return 'acme_hellobundle_producttype';
}
}
And all works great when i render the form, but now i want to change the widget of the "productCategory" to a text widget, because the user need to write the number that is the primary key of the productCategory.
But when i do it, and complete the form, i got the following error.
Warning: spl_object_hash() expects parameter 1 to be object, string
given in
C:\xampp\htdocs\sym2\Symfony\vendor\doctrine\orm\lib\Doctrine\ORM\UnitOfWork.php
line 1358
Seems like the ORM fails reading a string of the PK, anyone have any little idea of what i must see to fix it. Thanks in advice :)
Your product entity has a relation to product category. So your form expects the category to be an entity and not a string. This is why you get the error expects parameter 1 to be object, string given.
To avoid this you can remove setDefaultOptions method. If you do so the form class will not know anymore that it is associated to a certain entity class. The pitfall of this is, that when you pass the entity to the form class it will not set the fields automatically.
However now you can enter the category id and handle it.
E.g.
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('productCategory');
}
public function getName()
{
return 'acme_hellobundle_producttype';
}
}
Now productCategory will be text widget automatically. However you will need to persist it on your own in the controller. But for this you might ask another question.
Notice, when you create the form, don't pass the product object. Have it like this
$form = $this->createForm(new ProductType(), array());
I have a form for user registration, and only username field is present in the form. And in my form, I wish to allow user input the username only. Nicename would be same as username on registration.
This form is bind to a User entity, i.e., in my form type class:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Some\Bundle\Entity\User',
));
}
entity User, which has a NotBlank constraint set for both username and nicename.
namespace Some\Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Constraints;
//...
class User
{
//...
/**
* #var string $username
*
* #ORM\Column(name="user_login", type="string", length=60, unique=true)
* #Constraints\NotBlank()
*/
private $username;
/**
* #var string $nicename
*
* #ORM\Column(name="user_nicename", type="string", length=64)
* #Constraints\NotBlank()
*/
private $nicename;
//...
However, if I build a form with only username but not nicename, on validation i.e. $form->isValid() it fails to validate.
To bypass this, I come up with the following:
namespace Some\Bundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Some\Bundle\Form\Type\RegisterType;
//...
class UserController extends Controller
{
//...
public function registerAction()
{
//...
$request = $this->getRequest();
$form = $this->createForm(new RegisterType());
if ($request->getMethod() == 'POST') {
// force set nicename to username.
$registerFields = $request->request->get('register');
$registerFields['nicename'] = $registerFields['username'];
$request->request->set('register', $registerFields);
$form->bind($request);
if ($form->isValid()) {
$user = $form->getData();
//persist $user, etc...
And in form type I add this to my buildForm method:
$builder->add('nicename', 'hidden');
But I find this very inelegant, leave some burden to the controller (extract from the request object, put in data, and put it back into the request object, ouch!), and user can see the hidden field if he were to inspect the source code of generated HTML.
Is there anyway that can at least any controller using the form type does not need do things like above, while retaining the entity constraints?
I cannot change the table schema which backs up the User entity, and I would like to keep the NotBlank constraint.
EDIT: After long hassle, I decided to use Validation groups and it worked.
class User
{
//...
/**
* #var string $username
*
* #ORM\Column(name="user_login", type="string", length=60, unique=true)
* #Constraints\NotBlank(groups={"register", "edit"})
*/
private $username;
/**
* #var string $nicename
*
* #ORM\Column(name="user_nicename", type="string", length=64)
* #Constraints\NotBlank(groups={"edit"})
*/
private $nicename;
Form Type:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Some\Bundle\Entity\User',
'validation_groups' => array('register', 'Default')
));
}
That 'Default' is needed or it ignores all other constraints I added in the form type buildForm method... Mind you, its case sensitive: 'default' does not work.
Though, I find that it is not enough (and sorry I didn't put it in my original question), because when I persist, I need to do this in my controller:
$user->setNicename($user->getUsername());
As a bonus, I move this from controller to Form Type level by adding a Form Event Subscriber
In form type buildForm method:
$builder->addEventSubscriber(new RegisterPostBindListener($builder->getFormFactory()));
And the RegisterPostBindListener class
<?php
namespace Some\Bundle\Form\EventListener;
use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
class RegisterPostBindListener implements EventSubscriberInterface
{
public function __construct(FormFactoryInterface $factory)
{
}
public static function getSubscribedEvents()
{
return array(FormEvents::POST_BIND => 'setNames');
}
public function setNames(DataEvent $event)
{
$data = $event->getData();
$data->setNicename($data->getUsername());
}
}
I think you should use validation groups.
In your User entity you can tell which field can be nullable:
/**
*#ORM\Column(type="string", length=100, nullable=TRUE)
*/
protected $someVar;
This way your view controllers don't need to do anything.
Forgot to mention. You can also define a PrePersist condition that initialises your nicename variable:
// you need to first tell your User entity class it has LifeCycleCallBacks:
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class User
{
...
/**
*#ORM\PrePersist
*/
public function cloneName()
{
$this->nicename = $this->username;
}
}
In this case, you should use a Callback assertion to create a custom validation rule.