I'm currently working with Symfony2 and I'm testing my project with PHPUnit.
I want to test an exception when a form is submitted with the wrong parameters or the URL isn't complete.
I went trough the documentation of Symfony2 and PHPUnit but didn't find any class/method to do so.
How can I change the value of a form's action? I want to use PHPUnit so the report created is up to date and I can see the coverage of my code.
EDIT:
To clarify my question, some new content.
How do I test the line starting with '>' in my controller? (throw $this->createNotFoundException('Unable to find ParkingZone entity.');)
When the user modifies the action link, in the controller the process will go trough the exception (or error message, if this action is chosen). How can I test this case?
Controller
/**
* Edits an existing ParkingZone entity.
*
* #Route("/{id}/update", name="parkingzone_update")
* #Method("post")
* #Template("RatpGarageL1Bundle:ParkingZone:edit.html.twig")
*/
public function updateAction($id)
{
$em = $this->getDoctrine()->getEntityManager();
$entity = $em->getRepository('RatpGarageL1Bundle:ParkingZone')->find($id);
if (!$entity) {
> throw $this->createNotFoundException('Unable to find ParkingZone entity.');
}
$editForm = $this->createForm(new ParkingZoneType(), $entity);
$deleteForm = $this->createDeleteForm($id);
$request = $this->getRequest();
$editForm->bindRequest($request);
if ($editForm->isValid()) {
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('parkingzone_edit', array('id' => $id)));
}
return array(
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
);
}
View:
<form action="{{ path('parkingzone_update', { 'id': entity.id }) }}" method="post" {{ form_enctype(form) }}>
<div class="control-group">
{{ form_label(form.name, 'Name', { 'attr': {'class': 'control-label'} } ) }}
<div class="controls error">
{{ form_widget(form.name, { 'attr': {'class': ''} } ) }}
<span class="help-inline">{{ form_errors(form.name) }}</span>
</div>
</div>
<div class="control-group">
{{ form_label(form.orderSequence, 'Rang', { 'attr': {'class': 'control-label'} } ) }}
<div class="controls error">
{{ form_widget(form.orderSequence, { 'attr': {'class': ''} } ) }}
<span class="help-inline">{{ form_errors(form.orderSequence) }}</span>
</div>
</div>
<div class="control-group">
{{ form_label(form.image, 'Image', { 'attr': {'class': 'control-label'} } ) }}
<div class="controls error">
{{ form_widget(form.image, { 'attr': {'class': ''} } ) }}
<span class="help-inline">{{ form_errors(form.image) }}</span>
</div>
</div>
{{ form_rest(form) }}
<div class="form-actions">
<button type="submit" class="btn btn-primary">Enregistrer</button>
Annuler
</div>
</form>
Symfony itself does not have any objects through which it is possible to manipulate the form's action as it is set in the html (twig files). However, twig provides the capability to dynamically change the form's action in the twig file.
The basic approach is for the controller to pass a parameter into the twig file via the render call. Then the twig file can use this parameter to set the form action dynamically. If the controller uses a session variable to determine the value of this parameter then by setting the value of this session variable in the test program it is possible to set the form action specifically for the test.
For example in the controller:
public function indexAction()
{
$session = $this->get('session');
$formAction = $session->get('formAction');
if (empty($formAction)) $formAction = '/my/normal/route';
...
return $this->render('MyBundle:XXX:index.html.twig', array(
'form' => $form->createView(), 'formAction' => $formAction)
);
}
And then, in the twig file:
<form id="myForm" name="myForm" action="{{ formAction }}" method="post">
...
</form>
And then, in the test program:
$client = static::createClient();
$session = $client->getContainer()->get('session');
$session->set('formAction', '/test/route');
$session->save();
// do the test
This isn't the only way, there are various possibilities. For example, the session variable could be $testMode and if this variable is set the form passes $testMode = true into the render call. Then the twig file could set the form action to one of two values depending on the value of the testMode variable.
Symfony2 makes a distinction between unit testing of individual classes and functional testing of application behaviour. Unit testing is carried out by directly instantiating a class and calling methods on it. Functional testing is carried out by simulating requests and testing responses. See symfony testing for further detail.
Form submission can only be tested functionally as it is handled by a Symfony controller which always operates in the context of a container. Symfony functional tests must extend the WebTestCase class. This class provides access to a client which is used to request URLs, click links, select buttons and submit forms. These actions return a crawler instance representing the HTML response which is used to verify that the response contains the expected content.
It is only appropriate to test that exceptions are thrown when carrying out unit tests as functional tests cover interaction with the user. The user should never be aware that an exception has been thrown. Therefore the worst case scenario is that the exception is caught by Symfony and in production the user is presented with the catch-all response "Oops, an error has occurred" (or similar customised message). However, this should really only occur when the application is broken and not because the user has used the application incorrectly. Therefore it is not something that would typically be tested for in a functional test.
Regarding the first scenario mentioned in the question - submitting a form with the wrong parameters. In this case the user should be presented with an error message(s) telling them what was wrong with their input. Ideally the controller should not be throwing an exception but symfony validation should be used to automatically generate error messages next to each field as appropriate. Regardless of how the errors are displayed this can be tested by checking that the response to submitting the form contains the expected error(s). For example:
class MyControllerTest extends WebTestCase
{
public function testCreateUserValidation()
{
$client = static::createClient();
$crawler = $client->request('GET', '/new-user');
$form = $crawler->selectButton('submit')->form();
$crawler = $client->submit($form, array('name' => '', 'email' => 'xxx'));
$this->assertTrue($crawler->filter('html:contains("Name must not be blank")')->count() > 0,
"Page contains 'Name must not be blank'");
$this->assertTrue($crawler->filter('html:contains("Invalid email address")')->count() > 0,
"Page contains 'Invalid email address'");
}
}
Regarding the second scenario - where the URL isn't complete. With Symfony any URL which does not match a defined route will result in a NotFoundHttpException. In development this will result in a message such as 'No route found for "GET /xxx"'. In production it will result in the catch-all 'Oops, an error has occurred'. It would be possible to test in development that the response contains 'No route found'. However, in practice it doesn't really make sense to test this as it's handled by the Symfony framework and is therefore a given.
EDIT:
Regarding the scenario where the URL contains invalid data which identifies an object. This could be tested (in development) like this in the unit test program:
$client = static::createClient();
$page = $client->request('GET', '/update/XXX');
$exceptionThrown = ($page->filter('html:contains("NotFoundException")')->count() > 0) && ($page->filter('html:contains("Unable to find ParkingZone entity")')->count() > 0);
$this->assertTrue($exceptionThrown, "Exception thrown 'Unable to find ParkingZone entity'");
If you just want to test that an exception has been thrown rather than a specific type / message you can just filter the html for 'Exception'. Remember that in production the user will only see "Oops, an error has occurred", the word 'Exception' will not be present.
Thanks to #redbirdo with his last answer, I found a solution without messing out with the controllers.
I only changed few lines in the templates.
ControllerTest
public function testUpdate()
{
$client = static::createClient();
$session = $client->getContainer()->get('session');
$session->set('testActionForm', 'abc');
$session->save(); // This line is important or you template won't see the variable
// ... tests
}
View
{% if app.session.has('testActionForm') %}
{% set actionForm = path('parkingzone_update', { 'id': app.session.get('testActionForm') }) %}
{% else %}
{% set actionForm = path('parkingzone_update', { 'id': entity.id }) %}
{% endif %}
<form action="{{ actionForm }}" {{ form_enctype(form) }} method="POST" class="form-horizontal">
// ... rest of the form
Related
This Symfony form question has been asked 100 times (and I've read ALL of the responses), but none are working for me. I have a class (Employer), a form (Preview.html.twig), and a controller (DefaultController.php). No matter what I try, I still get null values for the form fields. The form displays properly and I'm not saving to a database (I just want to dump the variables, then I'll move on to db action). This has consumed weeks of my life and any assistance is sincerely appreciated.
The Default Controller (DefaultController.php)
<?
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use App\Entity\Employer;
use App\Form\EmployerType;
class DefaultController extends AbstractController
{ /**
* #Route("/preview", name="preview")
*/
public function preview(Request $request)
{
$employer = new Employer();
$form = $this->createForm(EmployerType::class, $employer, ['csrf_protection' => false]);
$form->handleRequest($request);
//the next two lines were added to force the form to submit, which it wasn't doing prior to
if ($request->isMethod('POST')) {
$form->submit($request->request->get($form->getName()));
if ($form->isSubmitted() && $form->isValid()) {
$employer = $form->getData();
dump($form); /****** ALL ENTRIES FROM THIS DUMP ARE NULL. *****/
exit; /***Added to capture dump ******/
return $this->redirectToRoute('homepage'); /** Works when the exit is removed ***/
}
}
return $this->render('preview.html.twig',
['form'=> $form->createView()]
);
}}
The Employer Class (Employer.php)
namespace App\Entity;
class Employer
{
protected $companyName;
protected $companyAddress;
public function setCompanyName($companyName)
{ $this->companyName = trim($companyName); }
public function getCompanyName()
{ return $this->companyName; }
public function setCompanyAddress($companyAddress)
{ $this->companyAddress = trim($companyAddress); }
public function getCompanyAddress()
{ return $this->companyAddress; }
}
Form Builder (EmployerType.php)
<?php
namespace App\Form;
use App\Entity\Employer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class EmployerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('companyName', TextType::class, ['label' => 'Company Name', 'required' => false])
->add('companyAddress', TextType::class, ['label' => 'Address', 'required' => false])
->add('submit',SubmitType::class, ['label' => 'Submit & Preview'])
->getForm() //I've added and removed this line multiple times. Not sure if its needed.
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Employer::class,
]);
}
}
For Display (Preview.html.twig) ** Displays Form Correctly **
{{ form(form) }}
A few rabbit holes:
The site is running on localhost (Symfony, Apache, MySQL).
The form input is sent via POST.
The Default Controller redirects after the Submit; the "exit" was added to pause my code.
The form is not embedded. I've scaled back the entire project because I thought the embedded form was the issue.
I changed the method to PUT and can see the form values appended to the URL, but $employer = $form->getData() still populates $employer with null values.
I tried to get individual form fields upon submit using $form->get('companyName')->getData(); The data remains null.
I'm out of ideas on how to save the form data to the Employer object.
You must delete getForm() in EmployeType.
In DefaultController, delete the line that contains form->submit(). Here the employee that you initialized is the form which fills it automatically. To retrieve your employee, you no longer need to do $form->getData(). The employee is already full. You can check with dd($employer) instead of $employer = $form->getData()
I gave up and created a fresh instance of Symfony using Composer. Starting over led me to the issue. Something (I'm not yet confident of what) in my custom twig file was causing the problem. I'll update everyone once I figure it out.
Final Findings:
The name attribute for my form inputs were incorrect. The controller was expecting named input in the form of:
formName[formField] //Ex: employer[companyName]
and mine were the standard type generated by Twig (formName_formField)
The addition of:
<p>form.vars: </p>
{{ dump(form.vars) }}
in my Twig file led me to the answer. I modified the input using a custom form theme by first adding the following line to twig.yaml:
form_themes: ['custom_bootstrap.html.twig']
Then, in the custom file, I created a new instance for each type of input I use to override the defaults. For example, for checkboxes my code is:
{% use "form_div_layout.html.twig" %}
{%- block checkbox_widget -%}
<input type="checkbox" id="{{ form.vars.name }}" name="{{ full_name }}"
{%- if disabled %} disabled="disabled"{% endif -%}
{%- if required %} required="required"{% endif -%}
{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %}
{{ block('attributes') }}
/>
{%- endblock checkbox_widget -%}
You should be able to add the name values directly to your twig template without using a custom file. I really hope this helps someone save time.
I've inherited an app and I'm not (yet) a symfony expert. The app has a simple user entity (among other things, of course) and it has some unique constraints:
/**
* User
*
* #ORM\Table(name="users")
* #ORM\Entity
* #UniqueEntity(
* fields={"username", "school"},
* message="There's already a registered user with that login in this school."
* )
* #UniqueEntity(fields={"email"},
* message="That email is already registered")
* )
*/
When creating a form, and when violating such constraints, the messages are displayed on the form itself and the flow isn't back to the controller (which is perfectly OK).
I have a non-ORM property, with $plainPassword, which holds the entered text in the form, together with the usual $password keeping the crypted password. This is the form part for this field:
$builder->add(
'plainPassword',
TextType::class,
[
'required' => false,
'attr' =>
[
'autocomplete' => 'off',
],
'error_bubbling' => true,
]
)
;
Now, for the password, I have a custom validator, that can be seen in the next function:
public function isPasswordValid(string $p = null)
{
if (null==$p) {
$p = $this->getPlainPassword();
}
// If I'm not changing the password, there must be one previously
$success = strlen($this->getPassword())>0;
if (strlen($p)) {
$success = (strlen($p)>7 && preg_match('/[A-Za-z]/', $p) && preg_match('/[0-9]/', $p));
}
return $success;
}
This functions works nice by itself, but I want to attach it to the form so when the plainPassword field has an "invalid" password (i.e., the function returns false). So I've tried by using an annotation block right before the function:
/**
* #Assert\IsTrue(message = "The password is not a valid password")
*/
public function isPasswordValid(string $p = null)
{
if (null==$p) {
$p = $this->getPlainPassword();
}
// If I'm not changing the password, there must be one previously
$success = strlen($this->getPassword())>0;
if (strlen($p)) {
$success = (strlen($p)>7 && preg_match('/[A-Za-z]/', $p) && preg_match('/[0-9]/', $p));
}
return $success;
}
The message goes to form.errors, and I could show it when the form is called in non-modal mode in a very specific (but different) way, but the default form behaviour via ajax showing as pop-up even skips this behaviour, so I don't get this message. Then, I've tried with error_mapping in the field definition in the form to link the property and the function, but the result is the same (i.e, it's not shown).
I've even tried to build a custom validator. In all these cases, the behaviour when validating the password is OK, but when the password is an invalid one, the form is not submitted, but no error message at all (unlike the other fields defined in the #UniqueConstraint). So, I either Cancel or just enter a valid password and submit the form, but a user doesn't have that reference about the error (and the profiler or the dev logs don't show anything about this, so I have to figure out by changing the password).
So, there must be something I'm missing or it's simply that by the means I've tested it's not possible. I'm diving now with the expression language for a expression assert, but I'm running out of ideas and it's taking me a couple of days just for this, so I ask for your help at this point to find out what I did wrong, overlooked or didn't do yet.
Thanks in advance.
UPDATE: Added a custom validator, where I'm not sure if I manage correctly empty data or anything related.
// ValidPassword.php
namespace XXX\UsersBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class ValidPassword extends Constraint
{
public $message = 'InvalidPasswordMessage';
}
(I want the message to be handled by the translations file.)
// ValidPasswordValidator.php
namespace XXX\UsersBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use XXX\UsersBundle\Entity\User;
class ValidPasswordValidator extends ConstraintValidator
{
public function validate($p, Constraint $constraint)
{
$success = true;
if (strlen($p)) {
$success = (strlen($p)>7 && preg_match('/[A-Za-z]/', $p) && preg_match('/[0-9]/', $p));
}
if (!$success) {
$this->context->buildViolation($constraint->message)
->addViolation();
}
}
And in the entity:
use XXX\UsersBundle\Validator\Constraints as UserAssert;
...
/**
* #var string
* #UserAssert\ValidPassword
*/
private $plainPassword;
This is the part of the twig template to show the involved fields:
...
<div class="row">
<div class="col-md-4">
{{ form_row(form.name) }}
</div>
<div class="col-md-4">
{{ form_row(form.surname) }}
</div>
</div>
<div class="row">
<div class="col-md-4">
{{ form_row(form.email) }}
</div>
</div>
<div class="row">
<div class="col-md-4">
{{ form_row(form.plainPassword) }}
</div>
</div>
ANSWERING
Your code it almost done. Your trouble is that you are using
'error_bubbling' => true
This config changes the object for error to be appended to the parent field target or the form. So, in your case, errors in plainPasswordField will be added to the form instead to the field.
MORE INFORMATION
After made some tests I guess that there's a little bit confusing about Form Errors
and Form Field Errors.
If your view code does not have form_errors(form) or form(form), all form.vars.errors won't be shown.
Child form fields with error_bubbling=true or codes like:
// Generic Controller
$form->addError(new FormError('My Generic Form Error not associate with any field!'));
return $this->render('my_template.html.twig', [
'form' => $form->createView(),
]);
olny can be displayed in twig template with
form_errors(form)
Useful Tips:
When creating form templates debug your form view with form(form) (must be the first call on template) to show everything in the form object, like fields (with html form input|select,
field label, field errors, field helpers) and errors in the form itself
The doc say about form_rest() but it not show form.vars.errors
Use form.vars.errors|length to check for errors on form view variable in twig
I would like to render the same form multiple times to handle the same action for two different tabs.
The problem is that when I try, only the form of the first tab is shown, event if I change the id and name of the form.
I found out it's the expected behavior of symfony, but I still need it to work.
I found that it may works with a collection but don't get how it would work.
twig:
{{ form(contactForm, {'attr': {'id': 'contactFormId' ~ Client.Id}, 'name': "contactFormName" ~ Client.Id})}}
Form:
$this->contactForm = $this->createFormBuilder($contact, array('allow_extra_fields' =>true))
->add('Nom', TextType::class, array('mapped'=>false))
->add('Prenom', TextType::class, array('mapped'=>false))
->add('Telephone', TextType::class, array(
'label' => 'Téléphone'))
->add('Email', TextType::class)
->add('Ajouter', SubmitType::class)
->getForm();
It is an older question, but I just came across it facing a similar situation. I wanted to have multiple versions of one form object in a list view. For me the solution was to move the createView() call on the form object to the view instead of calling it in the controller. This is kind of a dirty solution regarding separation of concerns, but I thought to post it so it may help others anyway.
My controller action looks like this:
/**
* #Route("", name="cart_show")
* #Method("GET")
*/
public function showAction(Request $request)
{
/** #var CartInterface $cart */
$cart = $this->get('rodacker.cart');
$deleteForm = $this->createDeleteForm();
return $this->render(
'AppBundle:Cart:show.html.twig',
['cart' => $cart, 'deleteForm' => $deleteForm]
);
// ...
private function createDeleteForm()
{
return $this->createForm(
OrderItemDeleteType::class,
null,
[
'action' => $this->generateUrl('cart_remove_item'),
'method' => 'DELETE',
]
);
}
}
and in the view I set the form variable by calling the createView function on the form variable (deleteForm) passed from the controller:
{% for item in items %}
{% set form = deleteForm.createView %}
{{ form_start(form) }}
{{ form_widget(form.item, {'value': item.image.filename}) }}
<button type="submit" class="btn btn-xs btn-danger" title="Artikel entfernen">
<i class="fa fa-trash-o"></i> entfernen
</button>
{{ form_end(form) }}
{% endfor %}
Once you render a Symfony form, the same form will not render again.
I would suggest creating a form class and calling Controller::createForm() multiple times to create the desired amount of Form instances; you can call isSubmitted etc. on all forms independently.
http://symfony.com/doc/current/book/forms.html#creating-form-classes
Can anybody tell me why this code is not working, I do not find an example of this on internet.
{% for dp in dpList %}
<tr>
<td> //values </td>
<td>....</td>
...
<td>
{{ form_start(display) }}
{{ form_row(display.info, { value: dp.info }) }}
{{ form_rest(display) }}
{{ form_end(display) }}
</td>
</tr>
{% endfor %}
What I am trying to do is to show a button "Display" for each row which contains a hidden value of this row. It actually only shows the button into the first row.
Thanks!
I found a solution that suits my needs:
I create a form in a private function:
private function formShow()
{
$show = $this->createFormBuilder()
->setAction($this->generateUrl('admin_platform_show'))
->setMethod('POST')
->add('persno', 'hidden')
->add('Show', 'submit')
->getForm();
return $show;
}
Then in my controller I create an arrayList with a loop on each row I will have, so it will generate as much as forms that I need:
foreach ($idmList as $idm)
{
$show = $this->formShow()->createView();
$arrayForms[] = $show;
}
Then I send my arrayList to my view and access each form like this:
{{ form_start(arrayForms[loop.index0]) }}
{{ form_row(arrayForms[loop.index0].persno, { value: idm.persno }) }}
{{ form_rest(arrayForms[loop.index0]) }}
{{ form_end(arrayForms[loop.index0]) }}
That way I use a secured form made by symfony, containing a specific value of the row that I will reuse in another controller if I press the submit button.
Hope that helps somebody.
This is incorrect. A form object is a single instance of the form, you can't render it multiple times on the same page.
You should either have multiple form objects, or use something else instead of forms, like a link or a button with javascript.
I suggest using jQuery and the post() function.
I have an entity User and an entity Address. There is a relation One-to-Many between User and Address :
class User
{
/**
* #orm:OneToMany(targetEntity="Address")
*/
protected $adresses;
[...]
}
I have a class AddressType, and class UserType :
class UserType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('addresses', 'collection', array('type' => new AddressType()));
}
[...]
}
In my controller, I build form with :
$form = $this->get('form.factory')->create(new UserType());
... and create view with :
return array('form' => $form->createView());
I display form field in my twig template with :
{{ form_errors(form.name) }}
{{ form_label(form.name) }}
{{ form_widget(form.name) }}
[...]
Okay. Now, how to display fields for one or more addresses ? (it's no {{ for_widget(form.adresses.zipcode) }} nor {{ for_widget(form.adresses[0].zipcode) }} ...)
Any ideas ?
This is how I did it in my form template:
{{ form_errors(form.addresses) }}
{% for address in form.addresses %}
<div id="{{ 'address%sDivId'|format(loop.index) }}" class="userAddressItem">
<h5> Address #{{ loop.index }}</h5>
{{ form_errors(address) }}
{{ form_widget(address) }}
</div>
{% endfor %}
And I have a small action bar, driven by jQuery, that lets the user add and remove addresses. It is a simple script appending a new div to the container with the right HTML code. For the HTML, I just used the same output has Symfony but with updated index. For example, this would be the output for the street input text of the AddressType form:
<input id="user_addresses_0_street" name="user[addresses][0][street]" ...>
Then, the next index Symfony will accept is 1 so the new input field you add would look like this:
<input id="user_addresses_1_street" name="user[addresses][1][street]" ...>
Note: The three dots are a remplacement for required="required" maxlength="255" but could change depending on your needs.
You will need more HTML code than that to add a whole new AddressType to the DOM of the browser but this give you the general idea.
Regards,
Matt
I should top that up with the fact that if you want to dynamically add fields, you need to set the key 'allow_add' to true in your collection field in UserType :
...
$builder->add('addresses', 'collection', array(
'type' => new AddressType(),
'allow_add' => true
));
Just spent hours trying to figure out what was missing, and at the time i'm writing the doc does not mention this yet. Hope it'll help fellow developers.