[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.
Related
I have custom Form Type:
class ChoosyType extends AbstractType
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
$form = $event->getForm();
$parentForm = $form->getParent();
$data = $event->getData();
$newChoices = $this->createNewChoices($options['choices']);
$parentForm->add($form->getName(), ChoosyType::class, array_replace($options, [
'choices' => $newChoices,
'data' => $this->entityManager->getRepository($options['class'])->find($data)
]));
});
}
When I submit my form - the change does not happen.
This is resulting only when I add the field to the parent form.
If I do not do this all works fine with the given choices.
Means when I add the new field - the value gets lost.
Did I something wrong or is this a know limitiation?
It is solved!
You just can not overwrite the same field from within the field itself on presubmit, sumbmit or aftersubmit.
It would result in an infinite loop.
Just do it inside of the parent, when you have to do some kind of custom logic.
I have a big form with time, date, select and EntityType fields in it. The form is not linked to an entity, but it do contain fields from other entities. In other words I have no data_class in the OptionsResolver of the FormType.
Here is my formtype: (showing just one field for simplicity)
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('company', EntityType::class, array(
'placeholder' => 'Companies',
'class' => 'AppBundle:Company',
'choice_label' => 'name',
'multiple' => true,
'query_builder' => function (EntityRepository $repository) {
return $repository->createQueryBuilder('c')->orderBy('c.name', 'ASC');
},
'required' => false
))
//... much more fields
}
public function configureOptions(OptionsResolver $resolver)
{
// This form is not linked to an Entity
// Therefore no `data_class`
}
In the controller I can backup the data of a form. Called a FormState. I save a FormState to the database as follows:
namespace AppBundle\Controller;
class ReportController extends Controller
{
/** // ... */
public function listAction(Request $request)
{
$form = $this->createForm(ReportType::class);
$form->handleRequest($request);
// Save the form to a FormState
if ($form->isSubmitted() && $form->getClickedButton()->getName() == 'saveFormState') {
$formStateManager = $this->get('app.manager.form_state');
// Get form data that we want to save
$data = $form->getData();
$name = 'Stackoverflow example'; // give it a name
$formState = $formStateManager->createNewFormState(); // Create new FormState Entity object
$formState->setName( $name );
$formState->setData( $data );
$formStateManager->save( $formState );
}
// ...
}
}
All this above works perfect. But now the tricky part, I have to set the backupdata back to the form. The user can select a previous form state from a list of FormStates. And then hit the load button.
The data attribute of a FormState object I try to load into the form is just a $form->getData() result. Thus a normal array without objects. Maybe that is the problem, but I can't get it to work whatsoever.
That data array is what I trying to load into the form so it takes its values over. I tried it via $form->setData(), or by creating a new form with $this->createForm(ReportType::class, $data). Both fail with the same error message: Entities passed to the choice field must be managed. Maybe persist them in the entity manager?.
I have tried two ways of adding the data:
First try, in controller:
namespace AppBundle\Controller;
class ReportController extends Controller
{
/** // ... */
public function listAction(Request $request)
{
if ($form->isSubmitted() && $form->getClickedButton()->getName() == 'loadFormState') {
// ...
$form = $this->createForm(ReportType::class, $formState->getData()); // <-- throws the error
$form->submit($formState->getData());
}
}
}
Second try, via FormEvent subscriber:
When I do it via a FormEvent subscriber, like below, I get the same error. Here is my code:
namespace AppBundle\Form\EventListener;
class ReportFieldsSubscriber implements EventSubscriberInterface
{
// ...
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SUBMIT => array(
array('loadFormState'),
),
);
}
/**
* Load selected FormState in form
* #param FormEvent $event
*/
public function loadFormState(FormEvent $event)
{
$form = $event->getForm();
//...
// Choosen FormState
$formState = $formStateManager->findOneById($formStateId);
$formStateData = $formState->getData();
$form->setData($formStateData); // <-- Set data, trows error
// ...
}
}
As I said both solutions throwing the error: Entities passed to the choice field must be managed. Maybe persist them in the entity manager?
What is the preferred solution in this case? I can create a dummy Entity, and fix this problem, but it feels a bit ugly and not the way to go.
After days of fiddling I found the solution.
My struggles where not how to save form data, as I showed in the question. However, I made a mistake by saving view data to the database and trying to load that later. Have a good read here for what I mean: https://symfony.com/doc/current/form/events.html#registering-event-listeners-or-event-subscribers
So, I also rewrite the saving of a form state to be in the form subscriber. Note: remember my form has no data_class in the OptionsResolver settings of the FormType. So there is no auto datamapping and such.
In my initial question, you can see I have multiple submit buttons, and in the controller you can check which one was clicked easily. Because in the controller you have access to the getClickedButton() function. But not in the form subscriber. So I found a way to get the clicked button in the EventSubscriberInterface of the form:
class ReportFieldsSubscriber implements EventSubscriberInterface
{
/**
* Find which button was clicked
* #param $data
* #return string
*/
public function getClickedButton($data)
{
$requestParams = serialize($data);
if (strpos($requestParams, 'saveFormState') !== false) {
return 'saveFormState';
}
if (strpos($requestParams, 'loadFormState') !== false) {
return 'loadFormState';
}
if (strpos($requestParams, 'deleteFormState') !== false) {
return 'deleteFormState';
}
return "";
}
}
Below you find the actual solution how I saved and loadedthe form the right way. I hook them up in the FormEvent::PRE_SUBMIT stage. At that stage you can save and load Request data into the FormEvent. Perfect.
Saving and loading/prepopulating the form data via FormEvent::PRE_SUBMIT:
class ReportFieldsSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SUBMIT => array(
array('saveFormState'),
array('loadFormState'),
),
);
}
/**
* Save form data
* #param FormEvent $event
*/
public function saveFormState(FormEvent $event)
{
$data = $event->getData();
if ($this->getClickedButton($data) == 'saveFormState') {
$formStateManager = $this->formStateManager;
// ...
$formState = $formStateManager->createNewFormState();
// ...
$formState->setData($data);
$formStateManager->save($formState);
}
}
/**
* Load choosen form data
* #param FormEvent $event
*/
public function loadFormState(FormEvent $event)
{
$data = $event->getData();
if ($this->getClickedButton($data) == 'loadFormState') {
$formStateId = $data['formState']['formStates'];
if (is_numeric($formStateId)) {
$formStateManager = $this->formStateManager;
$formState = $formStateManager->findOneById(intval($formStateId));
// Choosen FormState data
$formStateData = $formState->getData();
$event->setData($formStateData); // <-- Call setData() on the FormEvent, not on the form itself
}
}
}
}
I am pretty sure this solution will save someone days of coding!
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.
Im using FOSRestBundle to build a REST API in Symfony.
In my tax Controller i have:
private function processForm(Request $request, Tax $tax )
{
$form = $this->createForm(new TaxType(),$tax);
$req = $request->request->all();
$form->submit($req['tax']);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($tax);
$em->flush();
return $this->getTaxAction($tax->getId());
}
return array(
'form' => $form,
'request' => $request->request->all()
);
}
This function called from the POST and PUT functions.
TaxType
class TaxType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('createDate')
->add('writeDate')
->add('name')
->add('amount')
->add('active')
->add('createUid')
->add('writeUid')
->add('company')
;
}
...
It worked fine so far, but now i added some extra column to the table (and proprty to the Tax Entity) like: to which company belongs the record, date of creation. This wont come from the client but set on server side.
How do i add eg createData?
I have tried
$req['tax']['createDate'] = new \DateTime("now");
but im getting:
{"code":400,
"message":"Validation Failed",
"errors":{"children":{"createDate":{"errors":["This value is not valid."],
"children":{"date":{"children":{"year":[],"month":[],"day":[]}},
"time":{"children":{"hour":[],"minute":[]}}}},
"writeDate":{"children":{"date":{"children":{"year":[],"month":[],"day":[]}},
"time":{"children":{"hour":[],"minute":[]}}}},"name":[],"amount":[],"active":[],"createUid":[],"writeUid":[],"company":[]}}}
from entity Tax.php
/**
* #var \DateTime
*/
private $createDate;
I guess im extending the request with the correct data type, but im getting validation error.
If I understand the question correctly you don't have to take the request but just change the property on the $tax-object.
You can also do this after the form validation if you want.
You can Remove add('createDate') from your form builder.
$builder
->add('createDate')
->add('writeDate')
->add('name')
->add('amount')
->add('active')
->add('createUid')
->add('writeUid')
->add('company')
Then set the createdDate value on your $tex object before persist.
if ($form->isValid()) {
$tax->setCreateDate(new \DateTime("now"));
$em = $this->getDoctrine()->getManager();
$em->persist($tax);
$em->flush();
return $this->getTaxAction($tax->getId());
}
Alternative Method
Or you can use Doctrine's Lifecycle Callbacks to achieve this.
Happy coding!!
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',
));
}