Symfony2 - Form classes: data_class doesn't seem to work - php

I am using a Form class to help generate a form. It is a password reset form.
This form needs to be tied to my User entity, once the form is submitted the password will be updated.
Everything is working with the code I have currently, yet in an attempt to strip out some code that I believe shouldn't be required (due to using the data_class option) my validation breaks (as the form seems to become "detached" form the entity)
Ok, so some code:
public function passwordResetAction(Request $request, $key = NULL)
{
$user = $this->getDoctrine()
->getRepository('DemoUserBundle:User\User')
->findOneBy(array('resetToken' => $key));
if (!$user) {
throw $this->createNotFoundException('No user found for reset token '.$key.'!');
}
$form = $this->createForm(new PasswordResetType(), $user);
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
if ($form->isValid()) {
$postData = $request->request->get('resetpass');
$newPassword = $postData['password'];
$encoder = new MessageDigestPasswordEncoder('sha1', false, 1);
$password = $encoder->encodePassword($newPassword, $user->getSalt());
$user->setPassword($password);
$newToken = base64_encode($user->getUsername().'|'.date('Y-m-d'));
$user->setResetToken($newToken);
$em = $this->getDoctrine()->getEntityManager();
$em->persist($user);
$em->flush();
return $this->redirect($this->generateUrl('password_reset_success'));
}
}
return $this->render('DemoUserBundle:User\Reset:password-reset.html.twig', array('key' => $key, 'form' => $form->createView()));
}
The piece of code that I'm interested in is "$form = $this->createForm(new PasswordResetType(), $user);" As you can see, I'm using the Form Type to construct the form and passing it the $user variable that attaches the form to the User entity. Now, as far as I can understand, I should not need this parameter set as I have the entity set in the Form Type (see below for the code)
namespace Demo\UserBundle\Form\Type\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\CallbackValidator;
class PasswordResetType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('password', 'password', array('label' => 'New Password'));
$builder->add('confirmPassword', 'password', array('label' => 'Confirm Password', 'property_path' => false));
$builder->addValidator(new CallbackValidator(function($form)
{
if($form['confirmPassword']->getData() != $form['password']->getData()) {
$form['confirmPassword']->addError(new FormError('Passwords must match.'));
}
}));
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Demo\UserBundle\Entity\User\User',
);
}
public function getName()
{
return 'resetpass';
}
}
So this form should be default attach to the User Entity (without the need for the $user variable) yet it doesn't work. Without that variable being passed, the validation simply doesn't work (an indicator that it isn't being attached to the user entity)
What gives? Any ideas folks?

This data_class option is used to set the appropriate data mapper to be used by the form. If you don't supply it, it guesses it based on the object you pass to it.
It doesn't free you from passing an object to the form. How do you think it would know which object you want to work with?

in the new symfony versions use another method signature (option resolver instead of $options variable):
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Demo\UserBundle\Entity\User\User',
));
}
Note for Symfony 2.7: the form API has slightly changed, so you should use this instead:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Demo\UserBundle\Entity\User\User',
));
}

Related

Set entity parameter before form validation

[SETTINGS]
Symfony 3
BoxEntity: [id, name]
CandyEntity: [id, name]
[PROBLEM]
Currently, when creating a new candy, I must choose a box as parent entity.
The thing is, I would like this choice to be automated.
The box is already registered in the database, and the session is holding the current box parameters to find it back easily.
But I can't figure out how to apply it to the candy entity once the data have been posted.
[FILES]
AppBundle/Controller/CandyController.php
public function newAction(Request $request) {
$$candy= new Candy();
$form = $this->createForm('AppBundle\Form\CandyType', $conference);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($candy);
$em->flush();
return $this->redirectToRoute('candy_show', array('id' => $candy->getId()));
}
return $this->render('candy/new.html.twig', array(
'candy' => $candy,
'form' => $form->createView(),
));
}
AppBundle/Form/CandyType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('nom')
->add('box'); //Remove from form, and set manually
}
I did read this page, but can't figure out how to do it properly.
If someone would be so kind as to give me a full example to solve my problem, it would be much appreciated.
You have multiple options to perform what you want. You could set the value after your form submits:
public function newAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$candy = new Candy();
$box = $em->find('AppBundle\Entity\Box', $this->get('session')->get('boxId'));
$form = $this->createForm('AppBundle\Form\CandyType', $candy);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// add the box entity to the candy
$candy->setBox($box);
$em->persist($candy);
$em->flush();
return $this->redirectToRoute('candy_show', array('id' => $candy->getId()));
}
return $this->render('candy/new.html.twig', array(
'candy' => $candy,
'form' => $form->createView(),
));
}
You could set it on the Candy entity before passing it to the createForm() call, although it may not stay on the entity after doing the form handleRequest() call:
$em = $this->getDoctrine()->getManager();
$candy = new Candy();
$box = $em->find('AppBundle\Entity\Box', $this->get('session')->get('boxId'));
$candy->setBox($box);
$form = $this->createForm('AppBundle\Form\CandyType', $candy);
$form->handleRequest($request);
You could do it in the way that you are attempting, in a form event. What you would want to do is inject the entity manager and session into your form and treat your form as a service:
public function CandyType extends AbstractType
{
private $em;
private $session;
public function __construct(EntityManager $em, SessionInterface $session)
{
$this->session = $session;
$this->em = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ... build the form
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) {
$form = $event->getForm();
$candy = $event->getData();
$box = $this->em->find('AppBundle\Entity\Box', $this->session->get('boxId');
$candy->setBox($box);
}
);
}
}
You might need to do that on the POST_SET_DATA or POST_SUBMIT event instead, but I'm not sure. Also I used $this->get('session') in the Controller, but depending on your Symfony version (> 3.3) you could inject that into your controller as a service as well.
Either way the main concept is to use Doctrine to grab your Box entity from the session itself using the stored box id in the session, then set that on your Candy entity. You could even use a hidden field to achieve the same results. As I said before there are lots of ways to solve your issue.

Symfony : Can I return null from Type / Form?

I have an entity form with Symfony :
class MyType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'LogicielBundle\Entity\FichierGroup',
'intention' => $this->getName() . '_token'
));
}
But in POST_SUBMIT event, I want to return null (no entity).
I tested this but not working :
$builder->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) {
.... my condition ...
$event->setData(null);
});
Can you help me ? Thanks :)
Could you please post you controller code?
Do you pass an object reference to createForm, or do you use $form->getData() ? In your case, you should stick to the second.
Try using SUBMIT event instead of POST_SUBMIT. As Symfony doc states it, "It can be used to change data from the normalized representation of the data.".
To my knowledge you can not change a submit form. (I may be wrong)
But if you want reset a form after submit, you can do it in your controller:
For example:
public function fooAction()
{
...
$form = $this->getEntityForm($entity);
...
$form->handleRequest($request);
if ($form->isSubmitted()) {
// Do something
...
// Reset your form with null as entity
$form = $this->getEntityForm(null);
}
...
return $this->render(
'yourfootwig.html.twig',
[
'form' => $form->createView(),
...
]
);
}
protected function getEntityForm($entity = null)
{
return $this->createForm(
'Foo:MyType',
$entity,
[...]);
}
Of course, you must adapt it with your own code.
Actually, if you want change data passed to form entity, the event should be listened on is FormEvents::PRE_SUBMIT. As described in the official doc.

Symfony access Entity Manager inside EventSubscriber

I have an Employer profile form and I am trying to link the State where Employer is located at with a City this part is working alright but the FormType got so long and this feature will be required at other places of the website so I decided to move this logic inside an EventSubscriber and reuse it where ever I need it.
Problem I am having is I am trying to wrap my head around how to inject EntityManager inside the EventSubscriber class.
I know I can add the following code inside my services.yml and that should do it buts its not working.
app.form.location:
class: AppBundle\Form\EventListener\AddStateFieldSubscriber
arguments: ['#doctrine.orm.entity_manager']
tags:
- { name: kernel.event_subscriber }
This is my EmployerProfileType where I am calling my addEventSubscriber which is AddStateFieldSubscriber()
class EmployerProfileType extends AbstractType
{
protected $em;
function __construct(EntityManager $em)
{
$this->em = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('firstName', TextType::class)
->add('lastName', TextType::class)
->add('companyName', TextType::class)
->add('companyProfile', TextareaType::class)
->add('companyLogo', FileType::class, array(
'data_class' => null
));
$builder->addEventSubscriber(new AddStateFieldSubscriber());
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\EmployerProfile',
));
}
public function getName()
{
return 'app_bundle_emp_profile_type';
}
}
This is my AddStateFieldSubscriber class where I need access to EntityManager
class AddStateFieldSubscriber implements EventSubscriberInterface
{
protected $em;
function __construct(EntityManager $em)
{
$this->em = $em;
}
public static function getSubscribedEvents()
{
// Tells the dispatcher that you want to listen on the form.pre_set_data
// event and that the preSetData method should be called.
return array(
FormEvents::PRE_SET_DATA => 'onPreSetData',
FormEvents::PRE_SUBMIT => 'onPreSubmit'
);
}
protected function addElements(FormInterface $form, States $province = null)
{
// Remove the submit button, we will place this at the end of the form later
// Add the province element
$form->add('state', EntityType::class, array(
'data' => $province,
'placeholder' => 'provide_state',
'class' => 'AdminBundle\Entity\States',
'mapped' => false)
);
// Cities are empty, unless we actually supplied a province
$cities = array();
if ($province) {
// Fetch the cities from specified province
$repo = $this->em->getRepository('AdminBundle:Cities');
$cities = $repo->findByStates($province, array('name' => 'asc'));
}
// Add the city element
$form->add('city', EntityType::class, array(
'placeholder' => 'provide_state_first',
'class' => 'AdminBundle\Entity\Cities',
'choices' => $cities,
));
}
function onPreSubmit(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
// Note that the data is not yet hydrated into the entity.
$province = $this->em->getRepository('AdminBundle:States')->find($data['state']);
$this->addElements($form, $province);
}
function onPreSetData(FormEvent $event) {
$account = $event->getData();
$form = $event->getForm();
// We might have an empty account (when we insert a new account, for instance)
$province = $account->getCity() ? $account->getCity()->getStates() : null;
$this->addElements($form, $province);
}
}
The error I get is
Catchable Fatal Error: Argument 1 passed to
AppBundle\Form\EventListener\AddStateFieldSubscriber::__construct()
must be an instance of Doctrine\ORM\EntityManager, none given, called
in
/Users/shairyar/Sites/clickjobboard/src/AppBundle/Form/EmployerProfileType.php
on line 48 and defined
I am injecting EntityManager via service then why do I get this error?
If inside EmployerProfileType I replace
$builder->addEventSubscriber(new AddStateFieldSubscriber();
to
$builder->addEventSubscriber(new AddStateFieldSubscriber($this->em));
then things start working fine.
I thought in Symfony we are supposed to inject the dependencies by creating services? So how can I inject EntityManager inside my AddStateFieldSubscriber() class
What am i doing wrong? may be I am over thinking about it.
Will appreciate any feedback.
I think you need to tag your AddStateFieldSubscriber as kernel.event_subscriber.
Then the binding inside the form in not needed at all.
Instead you need to check for the correct form, and jump out of the event listener method if the form is not one of your forms using that subscriber, since the Event would be triggered on any form then, not only the EmployerProfileType form.

Protect against "Overposting Forms" within Symfony

Overposting a form is a common way of manipulating data / hacking a site.
The cause for that possible security problem is the Form/Model Binder, that automatically binds a Form to an object. Within ASP.NET, I know how to protect against these sort of attacks.
We have a User Model, with the following fields: ID, Firstname, Lastname, Password. I want the user to be able to change his Firstname and Lastname.
As we know, this happens within Symfony (and ASP.NET MVC) with a Form/Model Binder, that takes the "names" of the forms and maps these values to the corresponding object fields.
Solution in ASP.NET, using the [Bind] expression on each "Post-Controller":
public async Task<ActionResult> Create([Bind(Include="FirstName,Lastname")] Employee employee)
How can I prevent this kind of attack within a Symfony application? How to tell the Model/Form binder, which post data should only be accepted / expected?
#Edit:
This question intended to know how to solve this kind of problem when using a FormType for multiple usecases, e.g. for Creation and Edit of an employee. I know that in general, Symfony Form Component already checks if there are any additional fields.
class FooType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['type'] === 'edit') {
$builder->add('editMe');
//More edit me fields
}
$builder->add('createMe');
//more create me fields
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setRequired(array(
'type'
));
$resolver->setDefaults(array(
'type' => 'create'
));
}
//For consistency
public function getName()
{
return 'foo';
}
}
There is no need for extra events since it would be an overkill.
Controller:
public function createFooAction(Request $request)
{
$form = $this->createForm(new FooType(), new Foo());
$form->handleRequest($request);
if ($form->isValid() && $form->submitted()) {
//flush form
}
return $this->render("AppBundle:Foo:create.html.twig", array(
'form' => $form
));
}
public function editFooAction(Request $request, $id)
{
$foo = ... //find($id)
$form = $this->createForm(new FooType(), $foo, array(
'type' => 'edit'
));
$form->handleRequest($request);
if ($form->isValid() && $form->submitted()) {
//flush form
}
return $this->render("AppBundle:Foo:edit.html.twig", array(
'form' => $form
));
}
Bonus
If you use the cookbook (read: normal) approach to symfony forms, you already have that protection, alongside with CSRF protecttion.
If you add an extra field to the request you will get the following error:
This form should not contain extra fields
Example Form:
class FooType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title');
//Class Foo also has a text attribute
}
}
You will create an edit or create form using the above class in your controller and only the fields added to the builder can be modified. This way Foo::$text can not be modified using the form.

How to map two symfony form field to one model method?

Hi stackexchange users,
I have a data (model) class which has two methods which look like this:
class ContactDetails {
public function setWebsite($address, $type) {
//do something...
}
public function getWebsite($type) {
//do something...
}
}
Now I want to create a form where the user can input a website address and choose a type (e.g. "private" or "business") for the address.
To make this possible I have created a custom form type like this
class ContactDetailsType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('type', 'text') //better: choice, but for the sake of demo...
->add('website', 'text')
;
}
public function getName() {
return 'ContactDetailsType';
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver
->setDefaults(array(
'data_class' => 'ContactDetails',
));
}
}
The controller then looks like this:
public function indexAction(Request $request) {
//generate completely new cost unit
$costunit = new ContactDetails();
//generate form
$form = $this->createForm(new ContactDetailsType(), $costunit);
$form->add('save', 'submit');
$form->handleRequest($request);
if ($form->isValid()) {
//yay!
}
}
This obviously doesn't work, as the form component doesn't know how to map these two fields from the type to the data model class.
Question: What is the best practise to map the data of two fields of a form to one method call in a data model class and vice-versa?
On your place a i would make both fields virtual in form and then use event listener to set data in entity.
Info about form events

Categories