Skip to content

Commit 3319c43

Browse files
committed
[Form] Added method Form::getClickedButton() to remove memory leak in FormValidator
1 parent 287a433 commit 3319c43

File tree

5 files changed

+132
-51
lines changed

5 files changed

+132
-51
lines changed

ButtonBuilder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
6060
*
6161
* @throws InvalidArgumentException If the name is empty.
6262
*/
63-
public function __construct($name, array $options)
63+
public function __construct($name, array $options = array())
6464
{
6565
if (empty($name) && 0 != $name) {
6666
throw new InvalidArgumentException('Buttons cannot have empty names.');

Extension/Validator/Constraints/FormValidator.php

Lines changed: 6 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,6 @@
2222
*/
2323
class FormValidator extends ConstraintValidator
2424
{
25-
/**
26-
* @var array
27-
*/
28-
private static $clickedButtons = array();
29-
3025
/**
3126
* @var ServerParams
3227
*/
@@ -52,10 +47,6 @@ public function validate($form, Constraint $constraint)
5247
return;
5348
}
5449

55-
// If the form was previously validated, remove it from the cache in
56-
// case the clicked button has changed
57-
unset(self::$clickedButtons[spl_object_hash($form)]);
58-
5950
/* @var FormInterface $form */
6051
$config = $form->getConfig();
6152

@@ -181,21 +172,15 @@ private static function allowDataWalking(FormInterface $form)
181172
*/
182173
private static function getValidationGroups(FormInterface $form)
183174
{
184-
$root = $form->getRoot();
185-
$rootHash = spl_object_hash($root);
186-
187175
// Determine the clicked button of the complete form tree
188-
if (!array_key_exists($rootHash, self::$clickedButtons)) {
189-
// Only call findClickedButton() once to prevent an exponential
190-
// runtime
191-
// https://github.com/symfony/symfony/issues/8317
192-
self::$clickedButtons[$rootHash] = self::findClickedButton($root);
193-
}
176+
$clickedButton = null;
194177

195-
$button = self::$clickedButtons[$rootHash];
178+
if (method_exists($form, 'getClickedButton')) {
179+
$clickedButton = $form->getClickedButton();
180+
}
196181

197-
if (null !== $button) {
198-
$groups = $button->getConfig()->getOption('validation_groups');
182+
if (null !== $clickedButton) {
183+
$groups = $clickedButton->getConfig()->getOption('validation_groups');
199184

200185
if (null !== $groups) {
201186
return self::resolveValidationGroups($groups, $form);
@@ -215,28 +200,6 @@ private static function getValidationGroups(FormInterface $form)
215200
return array(Constraint::DEFAULT_GROUP);
216201
}
217202

218-
/**
219-
* Extracts a clicked button from a form tree, if one exists.
220-
*
221-
* @param FormInterface $form The root form.
222-
*
223-
* @return ClickableInterface|null The clicked button or null.
224-
*/
225-
private static function findClickedButton(FormInterface $form)
226-
{
227-
if ($form instanceof ClickableInterface && $form->isClicked()) {
228-
return $form;
229-
}
230-
231-
foreach ($form as $child) {
232-
if (null !== ($button = self::findClickedButton($child))) {
233-
return $button;
234-
}
235-
}
236-
237-
return null;
238-
}
239-
240203
/**
241204
* Post-processes the validation groups option for a given form.
242205
*

Form.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ class Form implements \IteratorAggregate, FormInterface
9090
*/
9191
private $submitted = false;
9292

93+
/**
94+
* The button that was used to submit the form
95+
* @var Button
96+
*/
97+
private $clickedButton;
98+
9399
/**
94100
* The form data in model format
95101
* @var mixed
@@ -551,6 +557,20 @@ public function submit($submittedData, $clearMissing = true)
551557
if (isset($submittedData[$name]) || $clearMissing) {
552558
$child->submit(isset($submittedData[$name]) ? $submittedData[$name] : null, $clearMissing);
553559
unset($submittedData[$name]);
560+
561+
if (null !== $this->clickedButton) {
562+
continue;
563+
}
564+
565+
if ($child instanceof ClickableInterface && $child->isClicked()) {
566+
$this->clickedButton = $child;
567+
568+
continue;
569+
}
570+
571+
if (method_exists($child, 'getClickedButton') && null !== $child->getClickedButton()) {
572+
$this->clickedButton = $child->getClickedButton();
573+
}
554574
}
555575
}
556576

@@ -730,6 +750,25 @@ public function isValid()
730750
return true;
731751
}
732752

753+
/**
754+
* Returns the button that was used to submit the form.
755+
*
756+
* @return Button|null The clicked button or NULL if the form was not
757+
* submitted
758+
*/
759+
public function getClickedButton()
760+
{
761+
if ($this->clickedButton) {
762+
return $this->clickedButton;
763+
}
764+
765+
if ($this->parent && method_exists($this->parent, 'getClickedButton')) {
766+
return $this->parent->getClickedButton();
767+
}
768+
769+
return null;
770+
}
771+
733772
/**
734773
* {@inheritdoc}
735774
*/

Tests/CompoundFormTest.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313

1414
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
1515
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
16+
use Symfony\Component\Form\FormBuilder;
1617
use Symfony\Component\Form\FormError;
1718
use Symfony\Component\Form\Forms;
19+
use Symfony\Component\Form\SubmitButtonBuilder;
1820
use Symfony\Component\HttpFoundation\Request;
1921
use Symfony\Component\HttpFoundation\File\UploadedFile;
2022
use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer;
@@ -830,6 +832,88 @@ public function testGetErrorsAsStringDeep()
830832
$this->assertEquals("name:\n ERROR: Error!\nfoo:\n No errors\n", $parent->getErrorsAsString());
831833
}
832834

835+
public function testNoClickedButtonBeforeSubmission()
836+
{
837+
$this->assertNull($this->form->getClickedButton());
838+
}
839+
840+
public function testNoClickedButton()
841+
{
842+
$button = $this->getMockBuilder('Symfony\Component\Form\SubmitButton')
843+
->setConstructorArgs(array(new SubmitButtonBuilder('submit')))
844+
->setMethods(array('isClicked'))
845+
->getMock();
846+
847+
$button->expects($this->any())
848+
->method('isClicked')
849+
->will($this->returnValue(false));
850+
851+
$parentForm = $this->getBuilder('parent')->getForm();
852+
$nestedForm = $this->getBuilder('nested')->getForm();
853+
854+
$this->form->setParent($parentForm);
855+
$this->form->add($button);
856+
$this->form->add($nestedForm);
857+
$this->form->submit(array());
858+
859+
$this->assertNull($this->form->getClickedButton());
860+
}
861+
862+
public function testClickedButton()
863+
{
864+
$button = $this->getMockBuilder('Symfony\Component\Form\SubmitButton')
865+
->setConstructorArgs(array(new SubmitButtonBuilder('submit')))
866+
->setMethods(array('isClicked'))
867+
->getMock();
868+
869+
$button->expects($this->any())
870+
->method('isClicked')
871+
->will($this->returnValue(true));
872+
873+
$this->form->add($button);
874+
$this->form->submit(array());
875+
876+
$this->assertSame($button, $this->form->getClickedButton());
877+
}
878+
879+
public function testClickedButtonFromNestedForm()
880+
{
881+
$button = $this->getBuilder('submit')->getForm();
882+
883+
$nestedForm = $this->getMockBuilder('Symfony\Component\Form\Form')
884+
->setConstructorArgs(array($this->getBuilder('nested')))
885+
->setMethods(array('getClickedButton'))
886+
->getMock();
887+
888+
$nestedForm->expects($this->any())
889+
->method('getClickedButton')
890+
->will($this->returnValue($button));
891+
892+
$this->form->add($nestedForm);
893+
$this->form->submit(array());
894+
895+
$this->assertSame($button, $this->form->getClickedButton());
896+
}
897+
898+
public function testClickedButtonFromParentForm()
899+
{
900+
$button = $this->getBuilder('submit')->getForm();
901+
902+
$parentForm = $this->getMockBuilder('Symfony\Component\Form\Form')
903+
->setConstructorArgs(array($this->getBuilder('parent')))
904+
->setMethods(array('getClickedButton'))
905+
->getMock();
906+
907+
$parentForm->expects($this->any())
908+
->method('getClickedButton')
909+
->will($this->returnValue($button));
910+
911+
$this->form->setParent($parentForm);
912+
$this->form->submit(array());
913+
914+
$this->assertSame($button, $this->form->getClickedButton());
915+
}
916+
833917
protected function createForm()
834918
{
835919
return $this->getBuilder()

Tests/Extension/Validator/Constraints/FormValidatorTest.php

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -430,11 +430,11 @@ public function testUseValidationGroupOfClickedButton()
430430
));
431431

432432
$parent->add($form);
433-
$parent->add($this->getClickedSubmitButton('submit', array(
433+
$parent->add($this->getSubmitButton('submit', array(
434434
'validation_groups' => 'button_group',
435435
)));
436436

437-
$form->setData($object);
437+
$parent->submit(array('name' => $object, 'submit' => ''));
438438

439439
$context->expects($this->once())
440440
->method('validate')
@@ -733,11 +733,6 @@ private function getSubmitButton($name = 'name', array $options = array())
733733
return $builder->getForm();
734734
}
735735

736-
private function getClickedSubmitButton($name = 'name', array $options = array())
737-
{
738-
return $this->getSubmitButton($name, $options)->submit('');
739-
}
740-
741736
/**
742737
* @return \PHPUnit_Framework_MockObject_MockObject
743738
*/

0 commit comments

Comments
 (0)