How to use nested form handlers in Symfony 3 - php

I'm learning Symfony to move an old website from flat-PHP to a framework.
In the old website I have a page with two levels of forms: the first one is just a button with which the user accepts some conditions, while the second one sends a message to the website admin. When the user clicks the button in the first form, the page is refreshed and the second form appears. The important thing is that the user can't get access to the second form without pushing the first button.
Right now, the action method is this one:
/**
* #Route("/ask-consultation", name="ask_consultation")
*/
public function askConsultationAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$form = $form = $this->createFormBuilder()
->add('submit', SubmitType::class, array('label' => 'Confermo'))
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$consultation = new Consultation;
$form = $this->createForm(AskConsultationType::class, $consultation);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$consultation = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($consultation);
$em->flush();
return $this->redirectToRoute('homepage');
}
return $this->render('visitor/consultation/ask_step2.html.twig', [
'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..').DIRECTORY_SEPARATOR,
'sidebars' => $em->getRepository('AppBundle:Sidebar')->findAllOrderedBySortingPosition(),
'form' => $form->createView(),
]);
}
When I go to /ask-consultation it shows me the button to accept certain conditions; when I click the button, it shows me the form to send the message, but when I send it, I don't get redirected to homepage, but again to the first page of /ask-consultation.
I understand why this code doesn't work, but I can't understand how to make it work. One solution could be some sort of modal dialog for the first form, but if possible I'd prefer to handle all the passages in PHP. Is it possible to split the form handling without changing the route?
The most important thing in my case is that the user can't get to the second form without first having clicked on the first button.

Render everything in a single form but use javascript (jQuery) to hide the ask-consultation part of the form. When the user clicks "Confirm", then unhide the form. This avoids the controller altogether for the confirmation step. You don't seem to be recording this confirmation anyway, so just let the client side handle things. You can still check (on form submission) to see that the "confirmation" was accepted, for example by setting a hidden variable through javascript.

It turns out the answer was quite simple.
In my mind, I had to handle the second form inside the first if ($form->isSubmitted() && $form->isValid()), but of course that's not the path taken by PHP. Everytime the route gets loaded, askConsultationAction() is run from its first instruction. I simply had to initialize both forms (with different variable names, obviously), and handle the forms one after the other, rather than one inside the other.
This code works like a charm:
/**
* #Route("/ask-consultation", name="ask_consultation")
*/
public function askConsultationAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$consultation = new Consultation;
$conditions_form = $this->createFormBuilder()
->add('submit', SubmitType::class, array('label' => 'Confermo'))
->getForm();
$consultation_form = $this->createForm(AskConsultationType::class, $consultation);
$consultation_form->handleRequest($request);
$conditions_form->handleRequest($request);
if ($conditions_form->isSubmitted() && $conditions_form->isValid()) {
return $this->render('visitor/consultation/ask_step2.html.twig', [
'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..').DIRECTORY_SEPARATOR,
'sidebars' => $em->getRepository('AppBundle:Sidebar')->findAllOrderedBySortingPosition(),
'form' => $consultation_form->createView(),
]);
}
if ($consultation_form->isSubmitted() && $consultation_form->isValid()) {
$consultation = $consultation_form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($consultation);
$em->flush();
return $this->redirectToRoute('homepage');
}
return $this->render('visitor/consultation/ask_step1.html.twig', [
'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..').DIRECTORY_SEPARATOR,
'sidebars' => $em->getRepository('AppBundle:Sidebar')->findAllOrderedBySortingPosition(),
'form' => $conditions_form->createView(),
]);
}
Maybe, it would be even better if I used an else... if....

Related

How to submit form in embeded controller?

I want to make a newsletter in a footer with my service. To do that, I have to embed a controller in a view :
{{ render(controller('AppBundle:RegisterNewsletter:registerToTheNewsletter')) }}
The probleme is that when I submit my form, the page is refreshed but nothing else, even I insert a "die;"
public function registerToTheNewsletterAction(Request $request)
{
$form = $this->createFormBuilder()
->add('email', EmailType::class)
->add('subscribe', SubmitType::class)
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
var_dump($data);
die;
}
return $this->render('include/registerNewsletter.html.twig', array('form' => $form->createView()));
}
Thanks for your help
I've got a solution, when I said, I had to embed a controller in a view. In fact during the submitting, the page is refreshed and the data is lost.
So I change the action of the form to redirect the data on an other path, the data is processed in an other action and I redirect on the referer.
If you want more explains, ask me

How to get user ID (FOSUserBundle) in form builder in Symfony2?

I'm quite new here, be patient, please.
I'm trying to make notice board project in Symfony2 using FOSUserBundle.
I try to get logged user id to put it into form created with form builder (and then to MySQL database).
One of attempts is:
public function createNoticeAction(Request $request)
{
$notice = new Notice();
$form = $this->createFormBuilder($notice)
->add("content", "text")
->add("user_id","entity",
array("class"=>"FOS/UserBundle/FOSUserBundle:", "choice_label"=>"id"))
->add("isActive", "true")
->add("category", "entity",
array("class" => "AppBundle:Category", "choice_label" => "name"))
->add("save", "submit", array("label" => "Save"))
->getForm();
$form->handleRequest($request);
$em = $this->getDoctrine()->getManager();
$em->persist($notice);
$em->flush();
return $this->redirectToRoute('app_user_showuserpage');
}
I tried many solutions again and again and I get some error.
You already have the user object Symfony > 2.1.x
In you Controller like this:
$userId = $this->getUser()->getId();
...
$notice->setUserId($userId);
$em->persist($notice);
Don't ->add field in you FormBuilder, its not safely. Set this value in you Controller and don't ->add this field in FormBuilder
for symfony 3.2.13
have excelent solution (just because is working, but is dangerous if someone discover it in pure HTML)
1) first build YourFormType class.
add normal field in Forms/YourFormType.php (if not, formbuilder tell you that you passing smth not quite right (too many fields) ; -) )
$builder
->add(
'MyModelAddedById',
HiddenType::class,
[
'label' => 'echhh', //somehow it has to be here
'attr' => ['style' => 'display:none'], //somehow it has to be here
]
);
2) in your controller
public function addSomethingAction(Request $request){
$form = $this->createForm(MyModelFormType::class);
//set field value
$request->request->set("prodModelAddedById", $this->getUser()->getId());
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$product = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($product);
$em->flush();
$this->addFlash('success', 'record was added');
return $this->redirectToRoute('products');
}
return $this->render(
'default.add.form.html.twig',
[
'newprod' => $form->createView(),
]
);
}
explenation:
you are passing a field and variable to formbuilder (settig it already to default value!)
and important thing, becose of BUG in my opinion - you can't in your form type set method:
public function getBlockPrefix()
{
//return 'app_bundle_my_form_type';
}
because
$request->request->set
can't work properly if your POST data from form are in bag (parameterbag)
no entity managers, no services, no listeners...
hope it helps.

not passing validation - Symfony2 Form without a Class

After a thorough search im not seeing the problem.
I have a form im submitting, which isnt tied to an object, its just an emailer form. I want to validate the data. according to the docs the alternative way is to do this.
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
$builder
->add('firstName', 'text', array(
'constraints' => new Length(array('min' => 3)),
))
->add('lastName', 'text', array(
'constraints' => array(
new NotBlank(),
new Length(array('min' => 3)),
),
))
;
now thats done, but i CANNOT call $form->isValid on this in any shape or form (pun intended), because even though it passes the constraint violations, something seemingly invisible is still causing it to return invalid.
i feel like i might need to extract the form from the post first, and pass that through isValid() but i cant be sure.
heres my method code
/**
* #Route("/share", name="email_share")
* #Method({"POST"})
* #return \Symfony\Component\HttpFoundation\Response
*/
public function shareAction( Request $request, array $lessons){
if(!$lessons || !is_array($lessons))
{
throw new HttpException(404, "Whoops! shareAction didnt get the lessons");
}
//set up our form defaults here
$defaultData = array('comment' => 'Type your comment here');
//build the form here
$form = $this->createFormBuilder($defaultData)
->setAction($this->generateUrl('email_share'))
->add("emails", 'email', array(
'label' => "Recipient's email address (separate with a comma)",
'constraints' => array(
new Length(array('min' => 6, 'max' => 2040)),
new NotBlank(),
),
))
->add('comment', 'textarea', array(
'label' => "Leave a comment",
))
->add('name', 'text', array(
'label' => "Your name",
'constraints' => array(
new Length(array('min' => 3), 'max' => 254)),
new NotBlank(),
),
))
->add('email', 'email', array(
'label' => "Your email address",
'constraints' => array(
new Length(array('min' => 6, 'max' => 254)),
new NotBlank(),
),
))
->add('copy', 'checkbox', array(
'label' => "Send me a copy",
'required' => false,
))
->add('cancel', 'submit', array(
'label' => "Cancel",
))
->add('save', 'submit', array(
'label' => "Email Resources",
))
->getForm();
if ($this->getRequest()->isMethod('POST')) {
//data is already validated by constraints added when the form was created since we are not attaching this particular form to any object
$form->handleRequest($request);
//alternatively (makes no differene from the former)
//$form->submit($request->request->get($form->getName()));
if ($form->isValid())
{
//have YET to see this
echo 'valid';
exit;
}
else{
//echo 'not fuckin valie, WHY?';
//exit;
// get a ConstraintViolationList
$errors = $this->get('validator')->validate( $form );
$result = '';
//nothing returns here when form is valid against constraints, its just empty
echo $errors;
// iterate on it
foreach( $errors as $error )
{
$error->getPropertyPath() : the field that caused the error
$error->getMessage() : the error message
}
}
$data = $form->getData();
return $this->emailUser($data);
}
return $this->render('ResourceBundle:Default:resources.html.twig', array(
'form' => $form->createView(),
));
}
This is how im posting the data
function postForm($form, callback) {
/*
* Get all form values
*/
var values = {};
$.each($form.serializeArray(), function (i, field) {
values[field.name] = field.value;
});
/*
* Throw the form values to the server!
*/
$.ajax({
type: 'POST',
url: '/share',
data: values,
success: function (data) {
callback(data);
}
});
}
$(document).ready(function () {
//bind an event to submit on 'email resources' button
$('div#share form').submit(function (e) {
//disable symfonys default submit button hehaviour
e.preventDefault();
postForm($(this), function (response) {
//replace html here
// Is this where im going wrong? Do i need to replace the form here?
});
});
});
EDIT: Here is the pertinent portion of the main template code that calls the action in the first place
<div id="share" class="hidden" >
<h2>Share Resources</h2>
{% render url('email_share') %}
</div>
here is the form template code thats rendered in the shareAction (in its entirety currently)
{{ form(form) }}
was
{% if form | default %}
{{ form(form) }}
{% endif %}
{% if mail_response | default %}
{{ dump(mail_response) }}
{% endif %}
The hidden token input portion of the form
<input id="form__token" class="form-control" type="hidden" value="8QWLo8xaPZFCKHBJbuc6CGNIcfmpWyT-yFdWScrsiJs" name="form[_token]">
The two underscores worry me a bit (form__token)
EDIT2:
The problem is in the CSRF token somewhere. it could be the form input name, the token itself is already expired, or something else.
I pretty much narrowed it down by constructing my own form module like this
//set up our form defaults here
$defaultData = array('comment' => 'Type your comment here');
$session = new Session();
$secret = '123xyz';
$vendorDir = realpath(__DIR__ . '/../vendor');
$vendorFormDir = $vendorDir . '/symfony/form/Symfony/Component/Form';
$vendorValidatorDir =
$vendorDir . '/symfony/validator/Symfony/Component/Validator';
// create the validator - details will vary
$validator = Validation::createValidator();
$formFactory = Forms::createFormFactoryBuilder()
->addExtension(new HttpFoundationExtension())
//->addExtension(new CsrfExtension(new SessionCsrfProvider($session, $secret)))
->addExtension(new ValidatorExtension($validator))
->getFormFactory();
//build the form here
$form = $formFactory->createBuilder('form', $defaultData)
->setAction($this->generateUrl('email_share'))
->setMethod('POST')
->add("emails", 'email', array(
//......
//same as above for the rest......
The form FINALLY passes validation like this, and when i uncomment the line
->addExtension(new CsrfExtension(new SessionCsrfProvider($session, $secret)))
i get the same error as i did before, that the CSRF token is invalid.
To me, this is pretty much pointing to somewhere in this module, or im not calling something, or extending something right, or the javascript is returning a form that is older than the CSRF module is expecting, or the hidden token form input has a name that is other than that what the CSRF module is looking for. I dont know enough about symfony's internals to diagnose this, this is why i come here for help. Does anybody see a potential issue?
EDIT:3 i feel like i shouldnt be using isValid(), as mentioned, i am not passing an object, im passing an array. see this URL http://symfony.com/doc/current/book/validation.html#validating-values-and-arrays. Im trying to figure out how to properly check against the constraints, and im thinking isValid() is NOT the way to go after all, or else im missing something fundamental.. I just cant figure if i only check against constraint errors, how can i use the CSRFprotection still, or is that only for objects or something?? Do i need to pass this in manually since im not using an object?
EDIT 4:
It looks like i might have uncovered the crux of the problem, yet i cant figure out how to solve it yet.
on this file Symfony\Component\Form\Extension\Csrf\CsrfProvider\DefaultCsrfProvider
i put some output to track the token, and it appears that the token is regenerating for the comparison, which seems like IT SHOULD NOT be the case I would expect that it would compare the passed down token to one in memory, but in fact, its being generated twice, once for the form, then once again for the comparison.
At first i suspected it was possible that the browser and the ajax are running from two different sessions, and a mismatch can be caused by this because i was using SessionCsrfProvider(), but after switching to ->addExtension(new CsrfExtension(new DefaultCsrfProvider($secret))) i had the same problem.
Is this a bug, am i going crazy, or am i missing something as simple as the form id in the building of the form or something?
heres the code, and the results i found from that code.
//Symfony\Component\Form\Extension\Csrf\CsrfProvider\DefaultCsrfProvider
public function isCsrfTokenValid($intention, $token)
{
echo '<pre>Warning, Symfony\Component\Form\Extension\Csrf\CsrfProvider\isCsrfTokenValid';
echo'<br>, here is out token handed down to compare<br>';
var_dump($token);
echo '<br>the new generated token thats being compared to is<br>';
var_dump($this->generateCsrfToken($intention));
echo '</pre>';
return $token === $this->generateCsrfToken($intention);
}
returns
//the form
array(6) { ["emails"]=> string(19) "email#email.net"
["comment"]=> string(2) "yo" ["name"]=> string(5) "me"
["email"]=> string(19) "email#email.net" ["copy"]=>
string(1) "1" ["_token"]=> string(40)
"a11e10eb323f7a4d19577e6d07e68be951ceb569" }
Warning,
Symfony\Component\Form\Extension\Csrf\CsrfProvider\isCsrfTokenValid ,
here is out token handed down to compare string(40)
"a11e10eb323f7a4d19577e6d07e68be951ceb569"
the new generated token thats being compared to is string(40)
"e83cdf94b15e63e822520b62402eb66e0b1f03d3"
The CSRF token is invalid. Please try to resubmit the form.
Blockquote
EDIT 5:
the problem has been traced to here, look at this code in the DefaultCsrfProvider
public function generateCsrfToken($intention)
{
return sha1($this->secret.$intention.$this->getSessionId());
}
public function isCsrfTokenValid($intention, $token)
{
return $token === $this->generateCsrfToken($intention);
}
The token can never be valid during an ajax call, unless a param is set in the generateCsrfToken() token method to allow passing of the session, to which you would want to pass that via ajax, like this
public function generateCsrfToken($intention, $session)
{
if(!$session)
{
$session = $this->getSessionId()
}
return sha1($this->secret.$intention.$session);
}
which i would think would completely reduce teh security of the whole idea of the CSRF in the first place.
is there another provide i can use for ajax calls in within the symfony framework? If were depending on the sesion, this pretty much leaves out both the SessionCsrfProvider class and the DefaultCsrfProvider class to process this, unless im missing something very obvious... should i just grab, pass, then reset the session on the ajax call????
Ok, after i figured this out thus far, i just found this post Symfony CSRF and Ajax ill see if i can make heads or tails from it.
To see errors you should render that form. In your code when form is invalid the method returns $this->emailUser($data); but it should render the form. Try this:
if ($this->getRequest()->isMethod('POST')) {
//data is already validated by constraints added when the form was created since we are not attaching this particular form to any object
$form->handleRequest($request);
//alternatively (makes no differene from the former)
//$form->submit($request->request->get($form->getName()));
if ($form->isValid())
{
//have YET to see this
echo 'valid';
//this is the place to process data
//$data = $form->getData();
//return $this->emailUser($data);
exit;
}
}
return $this->render('ResourceBundle:Default:resources.html.twig', array(
'form' => $form->createView(),
));
That should render invalid form again on submit and show error
You can also try this
if ($form->isValid())
{
//have YET to see this
echo 'valid';
exit;
}else{
echo '<pre>';
\Doctrine\Common\Util\Debug::dump($form->getErrorsAsString(), 9);
echo '</pre>';
}
To show errors in debug style if form is invalid.

Symfony 2 - add options into 'Choice' field after form is created

Can we edit the possible options for a choice field after the field has been created?
Let's say, the possible options for the choice field(a drop down box for categories) comes from my database. My controller would look like this:
public function addAction(Request $request){
//get the form
$categories = $this->service->getDataFromDatabase();
$form = $this->formFactory->create(new CategoryType(), $categories);
$form->handleRequest($request);
if ($form->isValid()) {
// perform some action, such as saving the task to the database, redirect
}
return $this->templating->renderResponse('TestAdminBundle:Categories:add.html.twig',
array('form' => $form->createView())
);
}
This works. $categories is populated as a dropdown box so the user can select a category. What I don't like about this code is that it has to hit the "getDataFromDatabase" service again when the user hits submit and the form validates the input. This feels unnecessary to me; ideally it should only need to hit the service when validation fails and the form has to be regenerated for the user. I'm hoping to make the controller look something like this:
public function addAction(Request $request){
//get the form
$form = $this->formFactory->create(new CategoryType());
$form->handleRequest($request);
if ($form->isValid()) {
// perform some action, such as saving the task to the database, redirect
}
$categories = $this->service->getDataFromDatabase();
$form->setData($categories); //this tells the choice field to use $categories to populate the options
return $this->templating->renderResponse('TestAdminBundle:Categories:add.html.twig',
array('form' => $form->createView())
);
}
You need to use EventSubscriber, check the docs, here: http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html#cookbook-form-events-underlying-data

Symfony2 invalid dynamic form with no error

I got a problem with a dynamic form on symfony2. I'm trying to generate some fields for a submitted form. In others words, the user enters some values, submits the form, and according to these values, my dynamics fields are added to this same form (which is, obviously, displayed a second time). To do that, I used this example from the cookbook : http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html#cookbook-form-events-submitted-data
So, here is my FormationType class
class FormationType extends AbstractType
{
private $em;
private $context;
public function __construct($em, $context) {
$this->em = $em;
$this->context = $context;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('date')
->add('type', 'choice', array(
'mapped' => false,
'choices' => Formationlist::getTypeTypes(false),
'empty_value' => false,
))
->add('cost')
->add('travelCost')
->add('maximum')
->add('location')
->add('schedule')
;
$formModifier = function(FormInterface $form, $type) {
$formationList = $this->em->getRepository('CoreBundle:FormationList')->findBy(array("year" => 1, "type" => $type));
$form->add('formationList', 'entity', array(
'label'=> 'Titre formation',
'choices' => $formationList,
'class' => 'CoreBundle:FormationList',
'property' => 'title',)
);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function(FormEvent $event) use ($formModifier) {
$data = $event->getForm();
$type = $data->get('type')->getData();
$formModifier($event->getForm(), $type);
}
);
$builder->get('type')->addEventListener(
FormEvents::POST_SUBMIT,
function(FormEvent $event) use ($formModifier) {
$type = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $type);
}
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'EXAMPLE\CoreBundle\Entity\Formation'
));
}
public function getName()
{
return 'example_corebundle_formationtype';
}
}
So, the two addEventListener work pretty well. The first time my form is displayed, the field in formModifier is not loaded, as expected. My controller class is the following one :
public function createAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$contextSrv = $this->get('example.service.context');
$context = $contextSrv->getContext();
$entity = new Formation();
$form = $this->createForm(new FormationType($em, $context), $entity);
$form->bind($request);
if ($form->isValid()) {
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('formation_show', array('id' => $entity->getId())));
}
return array(
'entity' => $entity,
'form' => $form->createView(),
);
}
Since one of my dynamic field can't be null, the first time the form is submitted, it can't be valid. So, the FormationType is loaded a second time. That means, if the field "type" was filled, my formModifier() function can load the dynamic field (formationList). Until there, everything works pretty well, and I got my new field.
But, after a second "submit" on the form...nothing happen. The page is just reloaded, and no errors are displayed.
I checked the form content with
var_dump($request->request->get('example_corebundle_formationtype'));
-> Every fields (including the dynamic one) are filled with valid values.
I also try this :
foreach($form->all() as $item) {
echo $item->getName();
var_dump($item->getErrors());
}
-> These lines don't show any error. But, the form is never valid.
var_dump($form->isValid());
-> It returns false. So the form is invalid.
Finally, if I remove the whole dynamic part, my form works.
I don't understand what's wrong. There is no errors displayed by the form, and the csrf token seems right. Did I miss something ? Thanks for your help.
I know this is a bit outdated but comes up quite high on Google.
The getErrors() metod returns only Form's global errors not error messages for the underlying fields, you need either getErrors(true) or more sophisticated method when using embeded forms in a form. Please see: https://knpuniversity.com/blog/symfony-debugging-form-errors for more information.
There is probably a validation error lying somewhere in your form.
Instead of your complicated calls to Form::getErrors() - which is not fully recursive, as the errors of any field deeper than the 2nd level will not be displayed - you should use Form::getErrorsAsString().
This is a debug method created by the Symfony guys for developers such as you, trying to understand where a validation error could lie in complex forms.
If no error is displayed although it should, this may be a form theming error. When creating a custom form theme, it is possible that a developper overrides or forgets to display the error block of a field.
Another possible source of the problem lies is the general display of the form. If you display your form using {{ form_widget(form) }}, then any error that bubbles to the top form will never be displayed. Make then sure that you use {{ form_row(form) }} instead.
I also encountered this problem a few times.
In my case I posted data in JSON format, so I had to do a request listener with a high priority which transforms json data into normal POST data, which is available in $request->request.
One scenario where the $form is invalid and there is no errors in also when the post data is empty, try to make a dump of $request->request->all() to see if you have the data.

Categories