Symfony2 form - date field validation doesn't work - forms

When I submit form the text field containing date isn't validated although I defined constraint in entity. What is incorrect? Do I need to write custom date validator for text field containing date?
In my form class I have
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('added', 'date', array(
'required' => false,
'widget' => 'single_text',
'format' => 'yyyy-MM-dd',
'attr' => array(
'class' => 'datepicker'
)
))
}
In entity
/**
* #var date
*
* #Assert\Date(message = "test")
* #ORM\Column(name="added", type="date", nullable=true)
*/
private $added;
And in controller (I need those errors listed)
$request = $this->getRequest();
$r = $this->getProfileRepository();
$profile = $id ? $r->find($id) : new \Alden\XyzBundle\Entity\Profile();
/* #var $profile \Alden\XyzBundle\Entity\Profile */
$form = $this->createForm(new ProfileType(), $profile);
if ($request->getMethod() == 'POST')
{
$form->bindRequest($request);
$errors = $this->get('validator')->validate($profile);
foreach ($errors as $e)
{
/* #var $e \Symfony\Component\Validator\ConstraintViolation */
$errors2[$e->getPropertyPath()] = $e->getMessage();
}
if (count($errors2))
{
...
} else {
$em = $this->getEntityManager();
$em->persist($profile);
$em->flush();
}

You may need to update your configuration. According to the Validation section of the Symfony2 book:
The Symfony2 validator is enabled by default, but you must explicitly enable annotations if you're using the annotation method to specify your constraints:
For example:
# app/config/config.yml
framework:
validation: { enable_annotations: true }

Related

Form submission : value is set on null

I am making an ad platform, I have just created a Booking entity and its form, but after that the form has been submitted, the value 'amount' is set on null while it should not be null.
I have created a prePersist function to set the amount property before flushing.
Here is the prePersist function in my entity Booking
* #ORM\PrePersist
*
* #return void
*/
public function prePersist()
{
if(empty($this->createdAt))
{
$this->createdAt = new \DateTime();
}
if(empty($this->amount))
{
$this->amount = $this->ad->getPrice() * $this->getDuration();
}
}
public function getDuration()
{
$diff = $this->endDate->diff($this->startDate);
return $this->days;
}
My BookingController
/**
* #Route("/annonces/{id}/booking", name="ad_booking")
* #IsGranted("ROLE_USER")
*/
public function booking(Ad $ad, Request $request, ObjectManager $manager)
{
$booking = new Booking;
$form = $this->createForm(BookingType::class, $booking);
$user = $this->getUser();
$booking->setBooker($user)
->setAd($ad);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid())
{
$manager->persist($booking);
$manager->flush();
return $this->redirectToRoute('booking_success', [
'id' => $booking->getId()
]);
}
return $this->render('booking/booking.html.twig', [
'ad' => $ad,
'bookingForm' => $form->createView()
]);
}
}
It does not work when the user is defined with $this->getUser(); in the submission and validity check. That's the first time it happens since I've started learning Symfony. I am sure I must have forgotten something but I spent so much time on thinking about what, that I can't see the answer.
and my BookingType form
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('startDate', DateType::class, [
'label' => 'Date de début de prestatation',
'widget' => 'single_text'
])
->add('endDate', DateType::class, [
'label' => 'Date de fin de prestatation',
'widget' => 'single_text'
])
;
}
While the form is submitted, it should call the prePersist function and then set the amount property, but it returns to be null. I really don't understand what I missed.
Since it seems your PrePersist is not fired, my guess is you may have forgotten the #ORM\HasLifecycleCallbacks() annotation on your entity.
/**
* #ORM\Entity(repositoryClass="App\Repository\BookingRepository")
* #ORM\HasLifecycleCallbacks()
*/
class Booking
{
...
}
I just found out that was wrong. It was linked to the automatic validation :
in the validator.yaml I commented the auto_mapping with
App\Entity\Booking: true
App\Entity\User: true
Now everything works fine !

EventListener on Embedded of different Forms

I have created a form with a collection of other forms in symfony 3. After Submitting I want to manipulate different Datas. How can I get access of it and how can I set them ?
This is how I tried it:
public function registerAction(Request $request)
{
$register = $this->createFormBuilder()
->add('user', 'AppBundle\Form\Type\UserType', array(
'data_class' => User::class,
))
->add('userdata', 'AppBundle\Form\Type\UserdataType', array(
'data_class' => Userdata::class,
))
->add('addresses', 'AppBundle\Form\Type\AddressesType', array(
'data_class' => Addresses::class,
))
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event){
$user = $event->getForm()->get('user')->getData();
$address = $event->getForm()->get('addresses')->getData();
$address->setSalutation($user->getSalutation());
})
->getForm();
$register->handleRequest($request);
if ($register->isSubmitted()) {
// do stuff
}
return $this->render('form/login_index.html.twig', [
'register' => $register->createView()
]);
}
In this example:
$user = $event->getForm()->get('user')->getData();
$address = $event->getForm()->get('addresses')->getData();
$address->setSalutation($user->getSalutation());
You should integrate the object you are working on as a parameter for your createFormBuilder($object) method.
For example:
$user = new User();
$form = $formFactory
->createBuilder($user)
->add('username', TextType::class);
And then you add your subscriber, in this case the $event->getData() is an object of the class User
$form->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
/** #var User $user */
$user = $event->getData();
$form = $event->getForm();
// Your logic here ...
})->getForm();
UPDATE:
If you have an embedded form (property in relation with the parent class), example UserInformations you can simply make a call with:
$form
->add('informations', UserInformationType::class)
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
/** #var User $user */
$user = $event->getData();
/** #var UserInformation $userInformation */
$userInformation = $user->getInformation();
$form = $event->getForm();
// Your logic here ...
})->getForm();
Good luck.

extend EntityType, set choices to selected Entity only

I've extended Symfony's EntityType as UserChooserType for use with my User entity and Select2. The choice list for the UserChooserType comes from an ldap query (via an ajax call), not a Doctrine query. So the field starts out blank.
The User entity is related to many different entities across my application. But if I want the UserChooserType to load with a current the selected User I have to add a listener to every form that uses it. e.g.:
class SiteType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$siteAdminOpts = array('label' => 'entity.site.admin', 'required'=>false);
//opts for the UserChooserType
$builder
->add('siteName', FT\TextType::class, array('label' => 'entity.site.name'))
->add('siteAdmin', UserChooserType::class, $siteAdminOpts )
//must be added to every form type that uses UserChooserType with mod for the datatype that $event->getData() returns
->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event){
$site = $event->getData();
$form = $event->getForm(); //SiteType
if($user = $site->getSiteAdmin()) $siteAdminOpts['choices'] = array($user);
$form->add('siteAdmin', UserChooserType::class, $siteAdminOpts);
});
}
//etc.
tldr;
I'd like to either:
set UserChooserType's choices option to the selected user in UserChooserType::configureOptions(), or
move ->addEventListener(...) into UserChooserType::buildForm().
Any idea how it might be done?
Here is the UserChooserType:
class UserChooserType extends AbstractType
{
/**
* #var UserManager
*/
protected $um;
/**
* UserChooserType constructor.
* #param UserManager $um
*/
public function __construct(UserManager $um){
$this->um = $um; //used to find and decorate User entities. It is not a Doctrine entity manager, but it uses one.
}
/**
* #inheritDoc
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if (!$data) return;
$user = $this->um->getUserByUserName($data);
if(!$user->getId()) $this->um->saveUser($user); //create User in db, if it's not there yet.
});
$builder->resetViewTransformers(); //so new choices aren't discarded
$builder->addModelTransformer(new CallbackTransformer(
function ($user) { //internal storage format to display format
return ($user instanceof User) ? $user->getUserName() : '';
},
function ($username) { //display format to storage format
return ($username) ? $this->um->getUserByUserName($username) : null;
}
));
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'class' => 'ACRDUserBundle:User',
'label' => 'ldap.user.name',
'choice_label' => function($user, $key, $index){
$this->um->decorateUser($user);
$label = $user->getDetail('displayName');
return $label ? $label : $user->getUserName();
},
'choice_value' => 'userName',
'choices' => [],
'attr' => array(
'class' => 'userchooser',
'placeholder' => 'form.placeholder.userchooser'
)
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'my_userchooser';
}
/**
* #inheritDoc
*/
public function getParent() {
return EntityType::class;
}
}
This helped me out:
https://symfony.com/doc/current/reference/forms/types/entity.html#using-choices
Here is how I used it (again with select 2):
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** #var ProductCollection $productCollection */
$productCollection = $builder->getData();
$builder
->add('products', EntityType::class,
[
'class' => Product::class,
'required' => FALSE,
'expanded' => FALSE,
'multiple' => TRUE,
'choices' => $productCollection->getProducts(), // We only preload the selected products. The rest come from api.
'attr' => [
'data-toggle' => 'select',
'data-options' => json_encode([
'ajax' => [
'url' => '/admin/product/autocomplete',
'dataType' => 'json'
]
])
],
'choice_label' => function (Product $product) {
return $product->getName();
},
'choice_attr' => function (Product $product) {
return [
'data-avatarsrc' => $product->getImage()->getUrl(),
'data-caption' => $product->getSkus()->first()->getSku(),
];
},
]
)
;
}
And here is my JS for the init of the select2 (copy/paste):
function() {
var elements = document.querySelectorAll('[data-toggle="select"]');
function templateResult(element) {
let avatarsrc, caption;
if (element.id && element.avatarsrc) {
avatarsrc = element.avatarsrc
caption = element.caption ? ' ' + element.caption : ''
}
else if (element.element) {
avatarsrc = element.element.dataset.avatarsrc ? element.element.dataset.avatarsrc : ''
caption = element.element.dataset.caption ? ' ' + element.element.dataset.caption : ''
}
if (!avatarsrc) {
return element.text + caption
}
let wrapper = document.createElement("div");
return wrapper.innerHTML = '<span class="avatar avatar-xs mr-3 my-2"><img class="avatar-img rounded-circle" src="' + avatarsrc + '" alt="' + element.text + '"></span><span>' + element.text + '</span><span class="badge badge-soft-success ml-2">' + caption + '</span>', wrapper
}
jQuery().select2 && elements && [].forEach.call(elements, function(element) {
var select, additionalOptions, select2options;
additionalOptions = (select = element).dataset.options ? JSON.parse(select.dataset.options) : {};
select2options = {
containerCssClass: select.getAttribute("class"),
dropdownCssClass: "dropdown-menu show",
dropdownParent: select.closest(".modal") ? select.closest(".modal") : document.body,
templateResult: templateResult,
templateSelection: templateResult
}
$(select).select2({...select2options, ...additionalOptions})
})
}(),
Some remarks first:
The field name UserChooserType is a bit elaborated. UserType is shorter and just as clear, and fits the Symfony naming conventions better
I wouldn't extend UserChooserType from EntityType. Doc says EntityType is specifically dedicated to entities coming from Doctrine. It adds magic so that the field is easily configured around Doctrine: transformers, automatic fetching of choices from the DB, etc. If your users are fully defined in the LDAP, I would instead recommend extending ChoiceType and populate your choices from the LDAP directly.
Coming back to your problem, what you want here is to leverage the almighty power of dependency injection, by declaring your field type as a service. After having done that, instead of:
$builder->add('siteAdmin', UserChooserType::class, $siteAdminOpts)
You will do (assuming you chose the form name user_chooser_type):
$builder->add('siteAdmin', 'user_chooser_type', $siteAdminOpts)
You need to inject the security.token_storage service into your field type. This is the service that holds the current user's information, you can access it like this:
$user = $securityTokenStorage->getToken()->getUser();
With this the population of the default value can happen at the field type level instead of its parent.
Using this, you could also add the LDAP choices at field type level too, by injecting a service able to communicate with the LDAP.
It should finally noted that all default symfony field types are also declared as services.
EDIT:
Previous answer is beside the point.
In Symfony, I'm not aware of any possible simple way to modify the choice list of a choice/entity field after it has been created. I don't think there is, and actually I might have done the same as you to tackle the issue you're describing.
My opinion is that you're already doing what needs to be done in this case.
If you're really motivated though, I think it might just be possible to move the event listener in the form type like this:
->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($options) {
if ($form->hasParent()) {
$data = $event->getData();
$form = $event->getForm();
$method = 'get' . ucfirst($form['action_name']);
if($user = $data->$method()) $siteAdminOpts['choices'] = array($user);
$form->getParent()->add($options['form_name'], UserChooserType::class, $siteAdminOpts);
}
});
Something like that. Not sure how that would work though.
I'm still interested in seeing if anyone comes up with a clean solution.

Unit Test Form WIth Repository Method

I am attempting to unit test one of my forms that I have which includes an entity form type on it. I would like to test the full form, but I keep on running into the error message - Expected argument of type "Doctrine\ORM\QueryBuilder", "NULL" given
Which is obvious, I need to somehow mock Doctrine\ORM\QueryBuilder as the return type for the entity form type. I am not quiet sure how I go about doing that though.
Here is the code of the Form -
<?php
namespace ICS\BackEnd\BoardBundle\Form;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class BoardCollectionType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', 'text', array(
'disabled' => TRUE
))
->add('member', 'entity', array(
'class' => 'MemberBundle:Members',
'property' => 'fullName',
'query_builder' => function(EntityRepository $er) {
return $er->findAllActiveMembers();
},
))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'ICS\BackEnd\BoardBundle\Entity\Board'
));
}
/**
* #return string
*/
public function getName()
{
return 'ics_boardbundle_board';
}
}
This is the test I am running on it -
<?php
namespace ICS\BackEnd\BoardBundle\Tests\Form;
use Doctrine\ORM\QueryBuilder;
use ICS\BackEnd\BoardBundle\Entity\Board;
use ICS\BackEnd\BoardBundle\Form\BoardCollectionType;
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
class BoardCollectionTypeTest extends TypeTestCase {
protected $repository;
protected function setUp()
{
parent::setUp();
$this->factory = Forms::createFormFactoryBuilder()
->addExtensions($this->getExtensions())
->getFormFactory();
}
public function testSubmittedValueData()
{
$formData = array(
'member' => NULL,
);
$type = new BoardCollectionType();
$form = $this->factory->create($type);
$object = new Board();
$object->createFromArray($formData);
// submit the data to the form directly
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($object, $form->getData());
$view = $form->createView();
$children = $view->children;
foreach (array_keys($formData) as $key) {
$this->assertArrayHasKey($key, $children);
}
}
protected function getExtensions()
{
$this->repository = $this->getMockBuilder('Doctrine\ORM\EntityRepository')
->disableOriginalConstructor()
->getMock();
$mockEntityManager = $this->getMockBuilder('\Doctrine\ORM\EntityManager')
->disableOriginalConstructor()
->getMock();
$mockEntityManager->expects($this->any())
->method('getRepository')
->will($this->returnValue($this->repository));
$classMetadata = $this->getMockBuilder('\Doctrine\Common\Persistence\Mapping\ClassMetadata')
->disableOriginalConstructor()
->getMock();
$mockEntityManager->expects($this->any())
->method('getClassMetadata')
->will($this->returnValue($classMetadata));
$mockRegistry = $this->getMockBuilder('Doctrine\Bundle\DoctrineBundle\Registry')
->disableOriginalConstructor()
->setMethods(array('getManagerForClass'))
->getMock();
$mockRegistry->expects($this->any())
->method('getManagerForClass')
->will($this->returnValue($mockEntityManager));
$mockEntityType = $this->getMockBuilder('Symfony\Bridge\Doctrine\Form\Type\EntityType')
->setMethods(array('getName'))
->setConstructorArgs(array($mockRegistry))
->getMock();
$mockEntityType->expects($this->any())
->method('getName')
->will($this->returnValue('entity'));
$this->assertQueryBuilderCalled();
return array(new PreloadedExtension(array(
$mockEntityType->getName() => $mockEntityType,
), array()));
}
protected function assertQueryBuilderCalled()
{
$em = $this->getMockBuilder('Doctrine\ORM\EntityManager')
->disableOriginalConstructor()->getMock();
$repo = $this->getMockBuilder('ICS\BackEnd\MemberBundle\Entity\Repository\MembersRepository')
->disableOriginalConstructor()->getMock();
$repo->expects($this->once())->method('findAllActiveMembers')
->will($this->returnValue(new QueryBuilder($em)));
/*$qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder')
->disableOriginalConstructor()
->getMock();
$query = $this->getMockBuilder('Doctrine\ORM\AbstractQuery')
->disableOriginalConstructor()
->setMethods(array('execute'))
->getMockForAbstractClass();
$query->expects($this->any())
->method('execute')
->will($this->returnValue(array()));
$qb->expects($this->any())
->method('getQuery')
->will($this->returnValue($query));
$this->repository->expects($this->any())
->method('findAllActiveMembers')
->will($this->returnValue($query));
$this->repository->expects($this->any())
->method('createQueryBuilder')
->will($this->returnValue($qb));*/
}
}
Thanks for any help!
I resolved the issue by changing the entity form type to choice. It made the test much easier to read and do. I injected the "choices" into the options of the form type.

Symfony2 Asset GreaterThan didn't work

I have an issue actually.
The property "Quantity" in Invetory entity should not be negative.
So I try to use the GreaterThan or GreaterThanOrEqual assert in my entity declaration.
In fact, I can validate negative quantities.
I don't understand.
The Entity :
/* src/Clicproxy/***Bundle/Entity/Inventory.php */
<?php
namespace Clicproxy\***Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Inventory
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="Clicproxy\***Bundle\Entity\InventoryRepository")
* #UniqueEntity(fields="audit, name", message="entity.inventory.unique")
*/
class Inventory
{
/* [...] */
/**
* #var integer
*
* #ORM\Column(name="quantity", type="integer", nullable=true)
* #Assert\GreaterThan(value = 1)
*/
private $quantity;
[...]
The FormType :
/* src/Clicproxy/***Bundle/Form/InventoryCollabType.php */
<?php
namespace Clicproxy\***Bundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class InventoryCollabType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('quantity', null, array('label' => 'entity.inventory.quantity'))
->add('pageCostBlack', null, array('label' => 'entity.inventory.pagecostblack'))
->add('pageCostColor', null, array('label' => 'entity.inventory.pagecostcolor'))
->add('avMonthPagesBlack', null, array('label' => 'entity.inventory.avmonthpagesblack'))
->add('avMonthPagesColor', null, array('label' => 'entity.inventory.avmonthpagescolor'))
;
}
/* [...] */
}
The Controller :
public function configAction (Request $request, $slug)
{
$em = $this->getDoctrine()->getManager();
$audit = $em->getRepository('Clicproxy***Bundle:Audit')->findOneBy(array('slug' => $slug));
if (!$audit instanceof Audit) {
throw $this->createNotFoundException('wizard.config.notfound');
}
$audit->addInventoriesFromEquipments($em->getRepository('Clicproxy***Bundle:Equipment')->findBy(array(), array('optimized' => 'ASC', 'name'=> 'ASC')));
$form = $this->createCreateConfigForm($audit);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($audit);
foreach ($audit->getInventories() as $inventory) {
$inventory->setAudit($audit);
$em->persist($inventory);
}
$em->flush();
/* [...] */
return $this->redirect($this->generateUrl('wizard_result', array('slug' => $audit->getSlug())));
}
/* [...] */
return array(
'audit' => $audit,
'form' => $form->createView(),
'tabactive' => 2,
);
}
Does anyone have an idea about my context ?
Thanks for your support,
David.
EDIT :
Finaly I've write this code below, your opinion ?
public function configAction (Request $request, $slug)
{
$em = $this->getDoctrine()->getManager();
$audit = $em->getRepository('Clicproxy***Bundle:Audit')->findOneBy(array('slug' => $slug));
if (!$audit instanceof Audit) {
throw $this->createNotFoundException('wizard.config.notfound');
}
$audit->addInventoriesFromEquipments($em->getRepository('Clicproxy***Bundle:Equipment')->findBy(array(), array('optimized' => 'ASC', 'name'=> 'ASC')));
$form = $this->createCreateConfigForm($audit);
$form->handleRequest($request);
if ($form->isValid()) {
$validator = $this->get('validator');
$errors_messages = array();
foreach ($audit->getInventories() as $inventory)
{
$violations = $validator->validate($inventory);
if (0 < $violations->count())
{
$error_message = substr($violations, strpos($violations, ':')+2);
if (! in_array($error_message, $errors_messages, true)) {
$errors_messages[] = $error_message;
$this->get('session')->getFlashBag()->add('error', $error_message);
}
}
}
if (! $this->get('session')->getFlashBag()->has('error'))
{
$em = $this->getDoctrine()->getManager();
$em->persist($audit);
foreach ($audit->getInventories() as $inventory) {
$inventory->setAudit($audit);
$em->persist($inventory);
}
$em->flush();
/* [...] */
return $this->redirect($this->generateUrl('wizard_result', array('slug' => $audit->getSlug())));
}
}
return array(
'audit' => $audit,
'form' => $form->createView(),
'tabactive' => 2,
);
}
Thanks for your support.
You aren't validating your entity, just your form.
It's a common mistake to assume that when you call $form->isValid() that your Doctrine entity is being validated, but that's not the case.
You need to explicitly call the validator service and handle that separately from the validation of your form.
That would looks something like this:
$validator = $this->get('validator');
$errors = $validator->validate($inventory);
if (count($errors) > 0) {
// Handle errors here
}
For more information, take a look at the validation documentation.