How to Test a Form with Multiple Submit - symfony

I have a form with 2 submit
// src/Form/FooType.php
$builder
->add('mainsubmit', SubmitType::class, [])
->add('extrasubmit', SubmitType::class, [])
In my controller, I do some different treatment depending of the submit pressed
// src/Controller/FooController.php
if ($form->isSubmitted() && $form->isValid()) {
if ($form->get('extrasubmit')->isClicked()) {
// do some extra stuff
}
When I click on the extra button, I can see it the symfony Profiler in the request POST parameters "extrasubmit" => "".
Everything works fine.
I'm doing functional tests with the crawler.
Without trying to submit with the extra submit, it works fine, so we can assume my test doesn't have a typo.
How can I simulate the click on the extra submit ?
First Try:
$form = $crawler->filter('form')->form();
// [...]
$form['my_form_name[extrasubmit]'] = true;
$httpClient->submit($form);
// => InvalidArgumentException: Unreachable field "extrasubmit"
Second Try:
$form->get('my_form_name[extrasubmit]')->setValue("");
// => InvalidArgumentException: Unreachable field "extrasubmit"

Like often when have trouble with the DomCrawler, it is much better to fo throught $form->getPhpValues() which returns an array of values.
Then you just have to mimic the values given in the request
$values = $form->getPhpValues();
$values['my_form_name']['extrasubmit'] = '';
$httpClient->request(
$form->getMethod(),
$form->getUri(),
$values
);

According to code you can set the button via the setNode($domnode) method on the form. Sadly there is no function to find the nodes on the form object itself (since the node doesn't have to be a descendant of the form, that might not even help much).
So I guess something similar to
$form->setNode($crawler->filter('#extrasubmitbutton-id')[0]);
would set the submit button...

Related

Render controller and get form errors from child

I have a template where I render a widget which contains a form:
{{ render(controller('ApplicationDemoBundle:Demo:newWidget', {'demo' : entity })) }}
The newWidgetAction calls a createAction:
public function createAction(Request $request)
{
$entity = new Demo();
$form = $this->createCreateForm($entity);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('demo_show', array('id' => $entity->getId())));
}
return array(
'entity' => $entity,
'form' => $form->createView(),
)
// Something like this would be awesome with passing the form containing errors with
// return $this->redirect($this->getRequest()->headers->get('referer'));
}
Imagine the submitted form (user acts in show theme) produces an error. This would make a return to the newWidget template which does not display the full layout.
My question is now: What is the right way to pass the errors from the child controller (newWidget) to the main template (show)?, without modifying the showActions function parameter to pass the formerrors over there.
There is a similar thread to this question: Symfony2 render and form
In this case where sessions used but I'm more than curious if this is the way to go.
The problem is that each fragment (a sub-controller) uses a virtual request. This is to protect the original request from modification by possibly unexpected forwards, and a fragment is essentially a forward taking place during a rendering stage.
It is possible to access the top level request using:
$this->container->get('request'); then handing the request with the form in the fragment, but this may get very confusing very quickly if you are using multiple forms per page.
My strategy is to follow a convention that limits the number of validated form on a page to just one. Any other forms don't require validation, or it is otherwise impossible for a form to be submitted incorrectly (hacked forms would throw server side exceptions, but the user should only see those if they are being naughty).
Try to structure your template inheritance to accommodate navigation to forms, while always showing most of the same layouts and data. You can do this by expanding the use of fragments which gives the bonus of separating your display logic.

Drupal custom form twice on same page, i need the errors displayed in the one submitted and not on both

I've a custom module that builds a form with a couple of fields, so far so good.
In one of my pages, i print this form twice (different blocks), the form gets the same "form_id", so when i submit one of them and get an error, both of them get the error highlighted, and the fields populated.
I want that only the form i submit gets the errors, is there a way to do this?
Thanks!!
For anyone interested, to do this you need to use the hook_forms.
This hook only gets called when the form_id passed to a drupal_get_form doesn't exist, this is important, if you want to use this, make sure your calls use a non existing form_id, for example:
//Defining the form:
function mx_wtransnet_form_contacto($form, &$form_state, $block = null, $formType = null) {
}
I want to use this form multiple times and get different error handlers, instead of loading my form (mx_transnet_form_contacto), i'll call a non existing one:
$form = drupal_get_form("mx_wtransnet_form_contacto_invalid", "contacto-mini");
Then i create my hook:
function mx_wtransnet_forms($form_id, $args) {
$forms = array();
if (strpos($form_id, '_contacto_') !==false) {
$forms[$form_id] = array(
'callback' => 'mx_wtransnet_form_contacto',
);
}
return $forms;
}
This function will catch all my druapl_get_form calls that don't exist, so i can process/direct them, in my example, what i do is simply check that the form_id contains contacto and then set the callback for this form to the original function.
In this case better to create another form with different "form_id" but with the same submit handler.
Another case: when you output same form twice on the page it also may get JS errors because ID of form elements are the same.
In case you don't repeat the form code and its submit handler(DRY principle), I would recommend create a custom function that has the form array
function form_my_custom($form_id){
$form['my_first_field'] = array();
$form['my_second_field'] = array();
$form['#attributes']['id'] = $form_id;
$form['my_submit_button'] = array(
'#submit' => array('my_custom_form_submit')
);
return $form;
}
function my_block1_form(){
return my_custom_form('my_form_id_1');
}
function my_block2_form(){
return my_custom_form('my_form_id_2');
}
function my_custom_form_submit(&$form, &$form_state){
// your submit handler.
}

Symfony2 functional testing InvalidArgumentException: The current node list is empty

I get "InvalidArgumentException: The current node list is empty." running functional tests through PHPUnit. Here is test i wrote:
public function testAdd()
{
$client = static::createClientWithAuthentication('main');
$crawler = $client->request('GET', 'en/manage');
$send_button = $crawler->selectButton('submit');
$form = $send_button->form(array(
'PrCompany[email]' => 'test#example.ua',
'PrCompany[first_name]' => 'Anton',
'PrCompany[last_name]' => 'Tverdiuh',
'PrCompany[timezone]' => 'Europe/Amsterdam'
));
$form['PrCompany[companies][1]']->tick();
$client->submit($form);
$this->assertTrue($crawler->filter('html:contains("User is invited")')->count() > 0);
}
You can debug the problem by using var_dump($client->getResponse()->getContent());
Additionally, I think you should write this:
$crawler = $client->submit($form);
Otherwise you'll be testing the response of the first url, before form submission.
I was also struggling with this, and It appeared that the selectButton method triggered this error.
After reading up on the DOM Crawler docs I found that the selectButton method takes the actual button text as a string argument. So if your button is 'submit my form please', that will be your text.
It does take different parameters too, as shown below (taken from the docs)
A selectButton() method is available on the Crawler which returns another
Crawler that matches a button (input[type=submit], input[type=image],
or a button) with the given text.
EDIT
After finally successfully completing the test I would also recommend you follow this example for testing forms:
use Goutte\Client;
$client = new Client();
$crawler = $client->request('GET', 'https://github.com/login');
$form = $crawler->selectButton('Log in')->form();
$form['login'] = 'symfonyfan';
$form['password'] = 'anypass';
$crawler = $client->submit($form);
$this->assertTrue($crawler->filter('html:contains("Welcome Back")')->count() > 0);
The main difference being, I have used the Goutte bundle, which I installed with composer from the packagist (in my case I added "fabpot/goutte": "1.0.*#dev")
As a follow up to what #greg0ire wrote, check to see if
var_dump($client->getResponse()->getContent());
Returns a redirect page instead of the actual content. If so, you can add this:
$client->followRedirects(true);
I had the same problem with Silex application. I was looking for
$buttonCrawler = $crawler->selectButton('input[type="submit"]');
Instead, the correct way to do it is give the value of the button
$buttonCrawler = $crawler->selectButton('value_of_the_button');
For example, in your page:
<form>
...
<input type="submit" value="Click Me">
</form>
And in your tests:
$buttonCrawler = $crawler->selectButton('Click Me');
$form = $buttonCrawler->form();
...
I see question still dont have answer. I had the same problem.
In my case goutte was not able to do this request because input name is changed by javascript on the fly.
When goutte received html it saw one form. And when submitting with pre filled params, form input elements could not be matched by $form->setValues($params) so \InvalidArgumentException was thrown.
Solved by doing request by hand.
// $form->setValues($data);
// $this->getGoutte()->submit($form);
$data = array(
'input_name[key]' => 'value'
);
$this->getGoutte()->request($form->getMethod(), $form->getUri(), $params);
You can try to use Codeception with Symfony2 module. It provides flexible interface to Symfony2 functional tests and has better debugging features.
This error would occur when crawler can't find form element requested; Quite tricky when you are using, for instance, form builder as when run, it will create different input name:
$form = $this-> createFormBuilder($store)
->add('storeNumber','text')
->add('storeName','text')
->add('save', 'submit')
->getForm();
will output field name like:
form_storeNumber
which should be used in test class:
$form=$crawler->selectButton('save')->form();
$form['form_storeNumber']='10';

Need advice to get my logic correct in d7

Goal: Is to save information of a node which gets updated. We need to gather the node id of the node which is updated and also the user names of people who have bookmarked it.
Implementation:
I have managed to get the both this detail using flags and rules module. I made a custom module which implemented the hook to get this info.
I am getting stuck here:
Now I need to save the user name and the node id. I am still deciding if I want to use fields or the db layer.
One username can have multiple node id saved.
Now the problem is I don't know for sure how many nodes will be enough. It depends on the user. It can be 5 can be 500 or even 5000 node ids that might need to be saved for one user.
So how do I make provision for this ?
So I am stuck with the logic. How should I use the db layer or the fields in custom content type to save this ? and how should I do it ?
Please advice. I am using d7.
custom module code
/*
* Implementation of the hook_rules_action_info()
*
*/
function customvishal_rules_action_info()
{
$actions = array(
'customvishal_action_userdetail' => array(
'label' =>t('Custom function to send notifications'),
'group'=>t('Cusotm Code for sending notifications'),
'parameter'=> array(
'account'=> array(
'type'=>'user',
'label'=>t('Going to get user list'),
),
// for the node
'productdetail'=> array(
'type'=>'node',
'label'=>t('Passding the node data'),
),
)
),
);
return $actions;
}
/*
* The action function for the rules exampled hello world
*
*/
function customvishal_action_userdetail($account,$productdetail)
{
drupal_set_message(t('This user #username! has flagged it',
array('#username' => $account->mail)));
drupal_set_message(t('This node #nid has got updated',
array('#nid' => $productdetail->nid)));
// The above takes care of the node and the user information later I will put
// it in a db or something like that.
// end of the function customvishal_action_userdetail
}
It really seems like you should be using hook_node_update() and hook_node_insert() to for access to nodes that have just been added or updated.
If you wanted access to the node data just before it was saved, then hook_node_presave() would be the one to use.
I don't think you need presave though because you mentioned you needed the node ID, and presave does not have that for new nodes yet.
Here's a way to process new and updated nodes. The first 2 functions just hook into the right place and route the node to the 3rd function.
<?php
// hook into node inserts
function customvishal_node_insert($node) {
if ($node->type == 'mynodetype') {
customvishal_handle_data($node);
}
}
// hook into node updates
function customvishal_node_update($node) {
if ($node->type == 'mynodetype') {
customvishal_handle_data($node);
}
}
// custom handler for the nodes
function customvishal_handle_data($node) {
// load a user object of the node's author
$author = user_load($node->uid);
// now do what we need to do with $node and $user data
}
Remember you need to clear the Drupal cache for new hooks in your module to work in D7.

Preventing form_token from rendering in Drupal "GET" forms

Drupal inserts a form_token as a hidden field when it renders forms. The form_token is then checked on form submission to prevent cross-site request forgery attacks. The form data that is submitted is guaranteed to have come from the original form rendered by Drupal.
However, forms using the "GET" method shouldn't need this token. All it does is lengthen and uglify the resulting URL.
Is there any way of suppressing it?
Yes, there is a way, but use it consciously (see warning below):
If you create the form yourself, adding
$form['#token'] = FALSE;
to the form definition array should prevent a token from being generated in the first place.
If you are dealing with an existing form, you can bypass the token validation process by unsetting the '#token' element on hook_form_alter:
// Example for removal of token validation from login (NOTE: BAD IDEA!)
function yourmodule_form_alter(&$form, &$form_state, $form_id) {
if ($form_id == 'user_login_block') {
unset($form['#token']);
}
}
Warning: Given your question, I think there is a slight misconception concerning the difference (better, the lack of a difference) between GET and POST requests.
... on forms using the "GET" method
shouldn't need this token. All it does
is lengthen and uglify the resulting
URL.
This is wrong! GET and POST are just two different, but mostly equivalent methods of transmitting data from the client to the server. Since POST is better suited to transfer large amounts of data (or difficult formatted data), it is the established standard for submitting forms, but it is in no way safer/unsafer or more/less secure than GET requests. Both type of requests can be tampered with by malicious users in the same ways, hence both types should use the same protection mechanisms.
With a GET request, the token does exactly the same as with a POST request - it proves to the server that the submitted data comes from the same Browser on the same machine as the request he build the form for! So you should only remove it if you are sure that the request can not be misused via XSRF.
This worked for me. I had to unset all the form api elements and set the #token property to false. Notice the after_build function for unsetting the other properties.
function mymodule_form(&$form_state){
$form['name'] = array(
'#type' => 'textfield',
'#title' => 'name',
'#value' => 'name',
);
$form['#method'] = 'get';
$form['#action'] = url('someurl');
$form['submit'] = array('#type' => 'submit', '#value' => 'go');
$form['#token'] = false;
$form['#after_build'] = array('mymodule_unset_default_form_elements');
return $form;
}
function mymodule_unset_default_form_elements($form){
unset($form['#build_id'], $form['form_build_id'], $form['form_id']);
return $form;
}
The site I work on uses the Drupal 6 form API for custom search forms, so by removing the token and build id we were able to cache the results in memcache. Now we've moved to Acquia hosting, it's cached using Varnish.
To remove the form_token and form_build_id from your form and submit it as a GET request, use the following method:
<?php
function module_example_form($form_state, $form_id = NULL) {
// Form root settings.
$form = array();
// Set the submission callback for this form.
$form['#submit'][] = __FUNCTION__ . '_submit';
// Set the request method for this form to GET instead of the default
// of POST.
$form['#method'] = 'get';
// Remove unique form token so request can be cached. This is accompanied by
// code in hook_form_alter to ignore the token and remove the build_id.
$form['#token'] = FALSE;
// Submit button.
$form['go'] = array(
'#type' => 'submit',
'#value' => t('Go!'),
);
return $form;
}
/**
* Implements hook_form_alter().
*/
function module_form_alter(&$form, $form_state, $form_id) {
// Changes to the 'module_example_form' form.
if ($form_id == 'module_example_form') {
// Unset the hidden token field and form_build_id field.
unset($form['#token'], $form['form_build_id'], $form['#build_id']);
}
}
?>
I find that simply throwing away the CSRF token is not an option. We solved it using hook_theme_registry_alter() to overwrite the Drupal core theme_hidden() function so that the hidden form element 'form_token' is rendered as an <esi /> tag. The tag will cause Varnish to make a call to a PHP file which we allow to pass through the cache. This file will calculate the proper form token for the current user and will then output the HTML code for the hidden field. You can calculate this token without a Drupal bootstrap, but you will need a single DB query to fetch the *drupal_private_key* for your site, which is stored in the variable table.

Resources