symfony error message in form when using custom validation method - forms

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

Related

Symfony 5.1 $form->getData() Always Null (Basic Form)

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.

Laravel From Request Validaton doesn't show errors

My Laravel Framework is 5.4.19
I have followed the tutorial:
http://www.easylaravelbook.com/blog/2015/08/17/creating-and-validating-a-laravel-5-form-the-definitive-guide/
and created form request file tours2_create_tableRequest.php:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class tours2_create_tableRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'doc_fullnumber' => 'unique:tourists'
];
}
}
(it tests if "doc_fullnumber" from request has no duplicates in 'tourists" model)
I have added this code to the form blade.php file:
#if($errors->count() > 0 )
<div>
#foreach ($errors->all() as $error)
{{ $error }}
#endforeach
</div>
#endif
it seems to me that validation works fine (it redirects the user back to create form page when "doc_fullnumber" is not unique), but it doesn't pass $errors variable.
I've browsed Stackoverflow and found several topics with the same problem. They suggest to wrap all my routes (in web.php) in:
Route::group(['middleware' => ['web']], function () {
});
I've done it, but it doesn't help...
here is the form page, actually (temporary): http://f832ee57.ngrok.io/tours_2/create
(the "doc_fullnumber" field is the last one. You can add "2001" there to check validation).
my project on git: https://github.com/Sergey1983/first
Appreciate any help!
To make the unique rule work, you need to have `'doc_fullnumber' field in DB or you need to specify a custom name field:
'doc_fullnumber' => 'unique:tourists,custom_fullnumber_field'

Laravel, Form::select unable to update many to many relationship

I'm new to Laravel, and I'm being dumb on this for sure, cause i've read the documentation and i've searched all over google but i'm not getting how to go over this. I have a M:M relationship between galleries and artist. Inside each gallery edit page I have a form to update the name and url for the gallery that is working fine. In the same page I have 2 other select forms, one for adding artists to the gallery and another to remove artists, that need to update a pivot table called "galleries_artists". I created 2 custom methods for both these forms called "postAdd" and "postRemove" but I can't put them to work regardless of what I try.
Below is the code i have so far. Hope somebody can help me understand the dumb mistakes i'm making.
Model - Artist.php
class Artist extends Eloquent {
protected $fillable = array('name');
public static $rules = array(
'name'=>'required|min:2'
);
public function galeries() {
return $this->belongsToMany('Gallery', 'galeries_artists', 'artist_id', 'gallery_id', 'stand_id');
}
}
Model - Gallery.php
class Gallery extends Eloquent {
protected $fillable = array('name', 'stand_id', 'url');
public static $rules = array(
'stand_id'=>'required|integer'
);
public function stand() {
return $this->belongsTo('Stand');
}
public function artist() {
return $this->belongsToMany('Artist', 'galleries_artists', 'gallery_id', 'artist_id', 'stand_id');
}
}
Controller - GalleriesController.php
public function postAdd($id, $aid) {
$input = array_except(Input::all(), '_method');
$v = Validator::make(Input::all(), Artist::$rules);
if ($v->passes()) {
$gallery = Gallery::find($id);
$add_artist = Input::get();
$add_artist->galleries()->attach(Input::get('add_artist'));
$add_artist->save();
return Redirect::route('admin.galleries.edit')
->with('message', 'Artist added successfully.');
}
return Redirect::route('admin.galleries.edit')
->with('message', 'Something went wrong')
->withErrors($v)
->withInput();
}
public function postRemove($id, $aid) {
$input = array_except(Input::all(), '_method');
$v = Validator::make(Input::all(), Artist::$rules);
if ($v->passes()) {
$gallery = Gallery::find($id);
$remove_artist = Input::get();
$remove_artist->galleries()->detach(Input::get('remove_artist'));
$remove_artist->save();
return Redirect::route('admin.galleries.edit')
->with('message', 'Artist removed successfully.');
}
return Redirect::route('admin.galleries.edit')
->with('message', 'Something went wrong')
->withErrors($v)
->withInput();
}
edit.blade.php
Add Form
{{ Form::open(array('class' => '', 'method' => 'put', 'action'=> array('GalleriesController#postAdd', $gallery->id , $add_artist->id ))) }}
<div class="form-group">
{{ Form::label('Add Artist:') }}
{{ Form::select('add_artist', $other_artists_name, null, array('class'=>'form-control')) }}
</div>
{{ Form::button('Add Artist', array('type' => 'submit', 'class'=>'btn btn-primary')) }}
{{ Form::close() }}
edit.blade.php
Remove Form
{{ Form::open(array('class' => '', 'method' => 'put', 'action'=>array('GalleriesController#postRemove', $id , 'aid'))) }}
<div class="form-group">
{{ Form::label('Remove Artist:') }}
{{ Form::select('remove_artist', $gallery_artists_name, null, array('class'=>'form-control')) }}
</div>
{{ Form::button('Remove Artist', array('type' => 'submit', 'class'=>'btn btn-danger')) }}
{{ Form::close() }}
Routes.php
Route::post('admin/galleries/{galleries}/add/{aid}', 'GalleriesController#postAdd');
Route::post('admin/galleries/{galleries}/remove/{aid}', 'GalleriesController#postRemove');
Route::resource('admin/galleries', 'GalleriesController');
I've been doing so many changes to the code that a lot of things might be mixed up. Sorry if that's the case.
You are making it pretty difficult. Here is what I did, but with checkboxes, which allowed me to cut down on the number of functions and forms I needed to work with. I've skipped the validation, but what I do have was tested and seems to work fine.
Swapping out the checkboxes for a select shouldn't be too much additional work, but I'd suggest going with a multi-select in that case, because again, it would be much simpler to work with for you and much easier to use from the user's standpoint. Let me know if I should modify my answer if it has to be selects.
Controller
class ArtController extends BaseController {
public function getIndex($artist_id)
{
// Get our artist with associated galleries
$artist = Artist::find($artist_id);
$artist->load('galleries');
// Get all galleries to populate our checkboxes
$galleries = Gallery::all();
// Show form
return View::make('art.gallery_update_form')->with('artist', $artist)->with('galleries', $galleries);
}
public function postIndex($artist_id)
{
// Grab our artist
$artist = Artist::find($artist_id);
// Sync the galleries. If no galleries were chosen, send it an empty array. Sync will perform both write and delete operations for you in one shot. Very handy.
$artist->galleries()->sync(Input::get('galleries', array()));
// Reshow the form
return $this->getIndex($artist_id);
}
}
View
#section('content')
{{ Form::open() }}
<!-- Get a list of our ID's so we can check/uncheck the checkboxes as we go -->
<?php $artist_galleries = $artist->galleries->lists('id'); ?>
<!-- Create checkbox for each gallery -->
#foreach($galleries as $gallery)
{{ Form::label($gallery->id, $gallery->name) }}
<!-- 3rd parameter is where the magic happens, it's looking in our list of galleries
we created a few lines up for the id to see if the artist belongs to that gallery or not -->
{{ Form::checkbox('galleries[]', $gallery->id, in_array($gallery->id, $artist_galleries), array('id' => $gallery->id)) }}
<br />
#endforeach
{{ Form::submit() }}
{{ Form::close() }}
#stop
Routes
Route::get('art/{artist_id}', array('uses' => 'ArtController#getIndex'));
Route::post('art/{artist_id}', array('uses' => 'ArtController#postIndex'));
Edit
Just getting your postAdd() method to work, all that should be required is something like this...
public function postAdd($id, $aid)
{
$input = array_except(Input::all(), '_method');
$v = Validator::make(Input::all(), Artist::$rules);
if ($v->passes()) {
$gallery = Gallery::find($id);
$gallery->artists()->attach($aid);
return Redirect::route('admin.galleries.edit')
->with('message', 'Artist added successfully.');
}
return Redirect::route('admin.galleries.edit')
->with('message', 'Something went wrong')
->withErrors($v)
->withInput();
}
I may be a little confused on the purpose of this function. As I wrote it, it will attach a selected artist to a gallery. I'm not sure of the purpose of using input. It looks as though you may also be attempting to save a new artist as though you wish your users to be able to create a new artist and assign that artist to a gallery when the artist is created? If you are passing in the artist id and the gallery id, that should be all you need and there is no need for the Input class.
Usually though, you'd be only passing in the gallery id which you would have generated a link for and it would be in the URI and the artist would be passed in via the form, in which case you would need to use Input::get('add_artist') and that would keep your URI much cleaner as well.
The postRemove() function would be the exact same, except you'd want to use detach() instead. This is all assuming of course all the rest of the functionality which is responsible for passing in the gallery id and artist id are working as well as the relationships themselves you've already setup in your models.

Symfony 2 This form should not contain extra fields

I created a form using formBuilder in Symfony. I add some basic styling to the form inputs using an external stylesheet and referencing the tag id. The form renders correctly and processes information correctly.
However, it outputs an unwanted unordered list with a list item containing the following text: This form should not contain extra fields.
I am having a really hard time getting rid of this notice. I was wondering if someone can help me understand why it being rendered with my form and how to remove it?
Many thanks in advance!
Controller
$form = $this->createFormBuilder($search)
->add('searchinput', 'text', array('label'=>false, 'required' =>false))
->add('search', 'submit')
->getForm();
$form->handleRequest($request);
Twig Output (form is outputted and processed correctly
This form should not contain extra fields.
Rendered HTML
<form method="post" action="">
<div id="form">
<ul>
<li>This form should not contain extra fields.</li>
</ul>
<div>
<input type="text" id="form_searchinput" name="form[searchinput]" />
</div>
<div>
<button type="submit" id="form_search" name="form[search]">Search</button>
</div>
<input type="hidden" id="form__token" name="form[_token]" value="bb342d7ef928e984713d8cf3eda9a63440f973f2" />
</div>
</form>
It seems to me that you have the problem because of the token field. If it is so, try to add options to createFormBuilder():
$this->createFormBuilder($search, array(
'csrf_protection' => true,
'csrf_field_name' => '_token',
))
->add('searchinput', 'text', array('label'=>false, 'required' =>false))
->add('search', 'submit')
->getForm();
To find out the extra field use this code in controller, where you get the request:
$data = $request->request->all();
print("REQUEST DATA<br/>");
foreach ($data as $k => $d) {
print("$k: <pre>"); print_r($d); print("</pre>");
}
$children = $form->all();
print("<br/>FORM CHILDREN<br/>");
foreach ($children as $ch) {
print($ch->getName() . "<br/>");
}
$data = array_diff_key($data, $children);
//$data contains now extra fields
print("<br/>DIFF DATA<br/>");
foreach ($data as $k => $d) {
print("$k: <pre>"); print_r($d); print("</pre>");
}
$form->bind($data);
This message is also possible if you added/changed fields in your createFormBuilder() and press refresh in your browser...
In this case it's ok after sending the form again ;-)
I got the same message while having multiple forms on the same page. Turns out, symfony defaults to the name 'form' for all of them. Instead of using createFormBuilder, you can change the name of the form to avoid conflicts using
public FormBuilderInterface createNamedBuilder(string $name, string|FormTypeInterface $type = 'form', mixed $data = null, array $options = array(), FormBuilderInterface $parent = null)
See https://stackoverflow.com/a/13366086/1025437 for an example.
I ran into this error when creating a multi-step form.
When the step 1 form is submitted, $request->request contains acme_mybundle_myform array. This created a validation error and stopped the back, forward and form fields from populating correctly. Not to mention "this-form-should-not-contain-extra-fields"
I discovered this thanks to the code by nni6.
The solution in my case was inside the controller:
if ($form->isValid())
{
if($form->has('nextStep') && $form->get('nextStep')->isClicked())
{
$session->getFlashBag()->set('notice', 'Next clicked');
$registerType->incrementStep();
$request->request->remove('acme_mybundle_myform');
return $this->forward("AcmeMyBundle:Default:register", array($request));
}
....
}
I had the same error.
It was because I had a form which, by mistake, had a NULL name.
In the HTML, the name attribute would look like this:
<form name href="..." action"..."></form>
As simple as that.
Might not be the case for everyone, but worth to check.

modify form action with symfony2 and phpunit

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