Form submission : value is set on null - forms

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 !

Related

Symfony 5 dynamic form conditional default logic

I've an use case where i need some default conditional logic on my dynamic form build in Symfony 5.
Let me try to explain what my use case is and my problem with a simple form.
For example i've a form Product with two fields:
Part (choiceType => left, right)
Length (numberType)
On change all fields (:input) are being submitted through an Ajax request.
I've two controller methods one for visiting the page (form is being build), the other
is being called for rendering the form through the ajax request (handle conditional logic).
For the conditional logic part the following needs te be done
When part is left, default length needs to be 50
When part is right, default length needs to be 100
user could change default data
Setting the default data on length based on left or right is not the problem.
When left is selected, default length becomes 50. When changing the value to 55 (form is being submitted through every change) it becomes 50 again.
This behaviour is logic, but how could the default data been overwritten?
Above situation could also been described as give user default data with option to change it
form type
<?php
// ... namespace, use statments
class ProductType extends AbstractType
{
/**
* {#inheritDoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('part', ChoiceType::class, array(
'choices' => array(
'Left' => 'left',
'Right' => 'right',
)
));
$builder->add('length', NumberType::class);
$builder->addEventListener(FormEvents::POST_SET_DATA, function(FormEvent $event) use ($options)
{
$form = $event->getForm();
if(null === $product = $event->getData()) {
return;
}
switch($product->getPart()) {
case 'left': $defaultLength = 50; break;
case 'right': $defaultLength = 100; break;
default: $defaultLength = 0;
}
$form->get('length')->setData($defaultLength);
});
}
/**
* {#inheritDoc}
*/
public function getName(): string
{
return 'product';
}
/**
* {#inheritDoc}
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(array(
'data_class' => Product::class,
'translation_domain' => 'forms',
));
}
}
controller
// src/Controller/ProductController.php
// ... namespace, use statments
namespace App\Controller;
class ProductController extends AbstractController
{
public function productAction(Request $request): Response
{
$product = new Product();
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$product = $form->getData();
dd($product);
}
return $this->render('product_view.html.twig', array(
'form' => $form->createView()
));
}
public function productConfigureAjaxAction(Request $request): Response
{
$product = new Product();
$part = $request->request->get('product')['part'] ?? null;
$product->setPart($part);
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
// product_form.html.twig is an separated file and included in product_view.html.twig
// by making the form separated is could been used for an ajax response
return $this->render('product_form.html.twig', array(
'form' => $form->createView()
));
}
}

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.

Symfony2 - Set a selected value for the entity field

I'm trying to set a selected value inside an entity field. In accordance with many discussions I've seen about this topic, I tried to set the data option but this doesn't select any of the values by default:
class EventType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('place', 'entity', array(
'class' => 'RoyalMovePhotoBundle:Place',
'property' => 'name',
'empty_value' => "Choisissez un club",
'mapped' => false,
'property_path' => false,
'data' => 2
))
->add('begin')
->add('end')
->add('title')
->add('description')
;
}
// ...
}
By looking for more I've found that some people had to deactivate the form mapping to the entity. That seems logical so I tried to add 'mapped' => false to the options, without success...
If it can help, here's my controller:
class EventController extends Controller
{
// ...
public function addAction()
{
$request = $this->getRequest();
$em = $this->getDoctrine()->getManager();
$event = new Event();
$form = $this->createForm(new EventType(), $event);
$formHandler = new EventHandler($form, $request, $em);
if($formHandler->process()) {
$this->get('session')->getFlashBag()->add('success', "L'évènement a bien été ajouté.");
return $this->redirect($this->generateUrl('photo_event_list'));
}
return $this->render('RoyalMovePhotoBundle:Event:add.html.twig', array(
'form' => $form->createView()
));
}
}
And the EventHandler class:
class EventHandler extends AbstractHandler
{
public function process()
{
$form = $this->form;
$request = $this->request;
if($request->isMethod('POST')) {
$form->bind($request);
if($form->isValid()) {
$this->onSuccess($form->getData());
return true;
}
}
return false;
}
public function onSuccess($entity)
{
$em = $this->em;
$em->persist($entity);
$em->flush();
}
}
I'm a bit stuck right now, is there anyone who got an idea?
You only need set the data of your field:
class EventController extends Controller
{
// ...
public function addAction()
{
$request = $this->getRequest();
$em = $this->getDoctrine()->getManager();
$event = new Event();
$form = $this->createForm(new EventType(), $event);
// -------------------------------------------
// Suppose you have a place entity..
$form->get('place')->setData($place);
// That's all..
// -------------------------------------------
$formHandler = new EventHandler($form, $request, $em);
if($formHandler->process()) {
$this->get('session')->getFlashBag()->add('success', "L'évènement a bien été ajouté.");
return $this->redirect($this->generateUrl('photo_event_list'));
}
return $this->render('RoyalMovePhotoBundle:Event:add.html.twig', array(
'form' => $form->createView()
));
}
}
In order to option appear selected in the form, you should set corresponding value to entity itself.
$place = $repository->find(2);
$entity->setPlace($place);
$form = $this->createForm(new SomeFormType(), $entity);
....
For non-mapped entity choice fields, the method I found easiest was using the choice_attr option with a callable. This will iterate over the collection of choices and allow you to add custom attributes based on your conditions and works with expanded, multiple, and custom attribute options.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('place', 'entity', array(
//...
'choice_attr' => function($place) {
$attr = [];
if ($place->getId() === 2) {
$attr['selected'] = 'selected';
//for expanded use $attr['checked'] = 'checked';
}
return $attr;
}
))
//...
;
}
When you use the query_builder option, and the data option expects an collection instance, and you don't want to touch your controller by adding setDatas for only certain fields, and you already have your querybuilder and the ids of the repopulating options in your form type class, you can repopulate a selection as following:
// Querybuilder instance with filtered selectable options
$entities = $qb_all;
// Querybuilder instance filtered by repopulating options (those that must be marked as selected)
$entities_selected = $qb_filtered;
Then in your add() Method
'data' => $entities_selected->getQuery()->getResult(), // Repopulation
'query_builder' => $entities,
EDIT: Real use case example
You want to repopulate a checkbox group rendered with following elements:
Label: What is your favourite meal?
4 Checkboxes: Pasta, Pizza, Spaghetti, Steak
And you want to repopulate 2 Checkboxes:
Pizza, Steak
$qb_all would be a QueryBuilder instance with the all 4 selectable Checkboxes
$qb_filtered would be a new additional QueryBuilder instance with the repopulating Checkboxes Pizza, Steak. So a "filtered" version of the previous one.

Symfony forms. File upload

Trying to manage file upload with Entity, but i get this error:
Fatal error: Call to a member function move() on a non-object in /home/projectpath/src/BS/MyBundle/Entity/Items.php on line 327 Call Stack: 0.0002 333264 1. {main}() /home/projectpath/web/app_dev.php:0 0.0450 1158160...
Here's the entity class:
namespace BS\BackstretcherBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;
/**
* MB\MyBundle\Entity\Items
*
* #ORM\Table(name="items")
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*/
class Items
{
private $filenameForRemove;
/**
* #Assert\File(maxSize="60000000")
*/
public $file;
...
protected function getUploadDir()
{
return 'images/items/';
}
protected function getUploadRootDir()
{
return __DIR__.'/../../../../web/'.$this->getUploadDir();
}
public function getWebPath()
{
return null === $this->file ? null : $this->getUploadDir().'/'.$this->getNameEn();
}
public function getAbsolutePath()
{
return null === $this->file ? null : $this->getUploadRootDir().'/'.$this->getNameEn().'.jpg';
}
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->file)
{
$this->file = $this->getId() .'.'. $this->file->guessExtension();
}
}
/**
* #ORM\PostPersist()
* #ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->file)
{
return;
}
$this->file->move($this->getUploadRootDir(), $this->file);
unset($this->file);
}
/**
* #ORM\PostRemove()
*/
public function removeUpload()
{
if ($file = $this->getAbsolutePath())
{
unlink($file);
}
}
And the controller:
public function new_productAction(Request $request)
{
$product = new Items();
$product->setPrice(0);
$form = $this->createFormBuilder($product)
->add('Type', 'choice', array(
'choices' => array('1' => 'Product', '0' => 'Article'),
'required' => false,))
->add('Price', 'number')
->add('nameEn', 'text')
->add('file', 'file', array('label' => 'Image', 'required' => true))
->getForm();
if ($request->getMethod() == 'POST')
{
if ($form->isValid())
{
$form->bindRequest($request);
$em = $this->getDoctrine()->getEntityManager();
$em->persist($product);
$em->flush();
return new Response('<html><body>Success!</body></html>');
}
}
return $this->render('MyBundle:Default:admin_page.html.twig', array(
'form' => $form->createView(),
));
}
Symfony version: 2.1.0
Check your php.ini file and make sure both the post_max_size AND upload_max_filesize are set sufficiently large.
I don't suppose duke_nukem is worried about this anymore, 6 months down the line, but if someone else comes across this question, I was having the exact same problem and got a great answer to it here:
Error with file upload in symfony 2
Looks like duke_nukem and I made the same mistake. The preUpload() method should read:
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->file)
{
$this->path = $this->getId() .'.'. $this->file->guessExtension();
}
}
The present code converts $this->file to a string, causing the error. The path should actually be assigned to $this->path.
Sybio in the other question figured this out, not me. I just want to spread the love.
it's weird
your code is wrong in your controller. You have to bind your request to your form before validation. After that, you can retrieve your data
if ($request->getMethod() == 'POST')
{
//Note: bindRequest is now deprecated
$form->bind($request);
if ($form->isValid())
{
//retrieve your model hydrated with your form values
$product = $form->getData();
//has upload file ?
if($product->getFile() instanceof UploadedFile){
//you can do your upload logic here wihtout doctrine event if you want
}
$em = $this->getDoctrine()->getEntityManager();
$em->persist($product);
$em->flush();
return new Response('<html><body>Success!</body></html>');
}
}

Symfony2 form - date field validation doesn't work

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 }