merged branch bschussek/issue8111 (PR #8638)
This PR was merged into the 2.3 branch. Discussion ---------- [Form] Fixed patched forms to be valid even if children are not submitted | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #8111 | License | MIT | Doc PR | - #8362 was reverted because it introduces a failing test that is caused by a regression. This PR takes the alternative approach that * unsubmitted fields in the PATCH request remain unsubmitted * `isValid()` ignores unsubmitted children * `mapFormsToData()` ignores unsubmitted children In my opinion this is a more proper solution than #8362. Commits -------85330a6
[Form] Fixed patched forms to be valid even if children are not submitted50f201e
Revert "[Form] Fix of "PATCH'ed forms are never valid""
This commit is contained in:
commit
ef69619614
@ -78,9 +78,9 @@ class PropertyPathMapper implements DataMapperInterface
|
||||
$propertyPath = $form->getPropertyPath();
|
||||
$config = $form->getConfig();
|
||||
|
||||
// Write-back is disabled if the form is not synchronized (transformation failed)
|
||||
// and if the form is disabled (modification not allowed)
|
||||
if (null !== $propertyPath && $config->getMapped() && $form->isSynchronized() && !$form->isDisabled()) {
|
||||
// Write-back is disabled if the form is not synchronized (transformation failed),
|
||||
// if the form was not submitted and if the form is disabled (modification not allowed)
|
||||
if (null !== $propertyPath && $config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled()) {
|
||||
// If the data is identical to the value in $data, we are
|
||||
// dealing with a reference
|
||||
if (!is_object($data) || !$config->getByReference() || $form->getData() !== $this->propertyAccessor->getValue($data, $propertyPath)) {
|
||||
|
@ -500,14 +500,6 @@ class Form implements \IteratorAggregate, FormInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
// In order to process patch requests we need to "submit" null values
|
||||
// Later this value will be mapped via DataMapper and InheritDataAwareIterator
|
||||
if ($submittedData === null && !$clearMissing) {
|
||||
$this->submitted = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// The data must be initialized if it was not initialized yet.
|
||||
// This is necessary to guarantee that the *_SET_DATA listeners
|
||||
// are always invoked before submit() takes place.
|
||||
@ -545,12 +537,10 @@ class Form implements \IteratorAggregate, FormInterface
|
||||
}
|
||||
|
||||
foreach ($this->children as $name => $child) {
|
||||
$fieldValue = null;
|
||||
if (array_key_exists($name, $submittedData)) {
|
||||
$fieldValue = $submittedData[$name];
|
||||
if (array_key_exists($name, $submittedData) || $clearMissing) {
|
||||
$child->submit(isset($submittedData[$name]) ? $submittedData[$name] : null, $clearMissing);
|
||||
unset($submittedData[$name]);
|
||||
}
|
||||
$child->submit($fieldValue, $clearMissing);
|
||||
}
|
||||
|
||||
$this->extraData = $submittedData;
|
||||
@ -720,11 +710,13 @@ class Form implements \IteratorAggregate, FormInterface
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isDisabled()) {
|
||||
foreach ($this->children as $child) {
|
||||
if (!$child->isValid()) {
|
||||
return false;
|
||||
}
|
||||
if ($this->isDisabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
if ($child->isSubmitted() && !$child->isValid()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,38 +89,6 @@ abstract class AbstractFormTest extends \PHPUnit_Framework_TestCase
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return \PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected function getValidForm($name)
|
||||
{
|
||||
$form = $this->getMockForm($name);
|
||||
|
||||
$form->expects($this->any())
|
||||
->method('isValid')
|
||||
->will($this->returnValue(true));
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return \PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected function getInvalidForm($name)
|
||||
{
|
||||
$form = $this->getMockForm($name);
|
||||
|
||||
$form->expects($this->any())
|
||||
->method('isValid')
|
||||
->will($this->returnValue(false));
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
|
@ -22,8 +22,8 @@ class CompoundFormTest extends AbstractFormTest
|
||||
{
|
||||
public function testValidIfAllChildrenAreValid()
|
||||
{
|
||||
$this->form->add($this->getValidForm('firstName'));
|
||||
$this->form->add($this->getValidForm('lastName'));
|
||||
$this->form->add($this->getBuilder('firstName')->getForm());
|
||||
$this->form->add($this->getBuilder('lastName')->getForm());
|
||||
|
||||
$this->form->submit(array(
|
||||
'firstName' => 'Bernhard',
|
||||
@ -35,17 +35,51 @@ class CompoundFormTest extends AbstractFormTest
|
||||
|
||||
public function testInvalidIfChildIsInvalid()
|
||||
{
|
||||
$this->form->add($this->getValidForm('firstName'));
|
||||
$this->form->add($this->getInvalidForm('lastName'));
|
||||
$this->form->add($this->getBuilder('firstName')->getForm());
|
||||
$this->form->add($this->getBuilder('lastName')->getForm());
|
||||
|
||||
$this->form->submit(array(
|
||||
'firstName' => 'Bernhard',
|
||||
'lastName' => 'Schussek',
|
||||
));
|
||||
|
||||
$this->form->get('lastName')->addError(new FormError('Invalid'));
|
||||
|
||||
$this->assertFalse($this->form->isValid());
|
||||
}
|
||||
|
||||
public function testValidIfChildIsNotSubmitted()
|
||||
{
|
||||
$this->form->add($this->getBuilder('firstName')->getForm());
|
||||
$this->form->add($this->getBuilder('lastName')->getForm());
|
||||
|
||||
$this->form->submit(array(
|
||||
'firstName' => 'Bernhard',
|
||||
));
|
||||
|
||||
// "lastName" is not "valid" because it was not submitted. This happens
|
||||
// for example in PATCH requests. The parent form should still be
|
||||
// considered valid.
|
||||
|
||||
$this->assertTrue($this->form->isValid());
|
||||
}
|
||||
|
||||
public function testDisabledFormsValidEvenIfChildrenInvalid()
|
||||
{
|
||||
$form = $this->getBuilder('person')
|
||||
->setDisabled(true)
|
||||
->setCompound(true)
|
||||
->setDataMapper($this->getDataMapper())
|
||||
->add($this->getBuilder('name'))
|
||||
->getForm();
|
||||
|
||||
$form->submit(array('name' => 'Jacques Doe'));
|
||||
|
||||
$form->get('name')->addError(new FormError('Invalid'));
|
||||
|
||||
$this->assertTrue($form->isValid());
|
||||
}
|
||||
|
||||
public function testSubmitForwardsNullIfValueIsMissing()
|
||||
{
|
||||
$child = $this->getMockForm('firstName');
|
||||
@ -59,17 +93,14 @@ class CompoundFormTest extends AbstractFormTest
|
||||
$this->form->submit(array());
|
||||
}
|
||||
|
||||
public function testSubmitDoesNotSaveNullIfNotClearMissing()
|
||||
public function testSubmitDoesNotForwardNullIfNotClearMissing()
|
||||
{
|
||||
$child = $this->getMockForm('firstName');
|
||||
|
||||
$this->form->add($child);
|
||||
|
||||
$child->expects($this->once())
|
||||
->method('submit');
|
||||
|
||||
$child->expects($this->never())
|
||||
->method('setData');
|
||||
->method('submit');
|
||||
|
||||
$this->form->submit(array(), false);
|
||||
}
|
||||
@ -124,37 +155,6 @@ class CompoundFormTest extends AbstractFormTest
|
||||
$this->assertFalse($this->form->isEmpty());
|
||||
}
|
||||
|
||||
public function testValidIfSubmittedAndDisabledWithChildren()
|
||||
{
|
||||
$this->factory->expects($this->once())
|
||||
->method('createNamedBuilder')
|
||||
->with('name', 'text', null, array())
|
||||
->will($this->returnValue($this->getBuilder('name')));
|
||||
|
||||
$form = $this->getBuilder('person')
|
||||
->setDisabled(true)
|
||||
->setCompound(true)
|
||||
->setDataMapper($this->getDataMapper())
|
||||
->add('name', 'text')
|
||||
->getForm();
|
||||
$form->submit(array('name' => 'Jacques Doe'));
|
||||
|
||||
$this->assertTrue($form->isValid());
|
||||
}
|
||||
|
||||
public function testNotValidIfChildNotValid()
|
||||
{
|
||||
$child = $this->getMockForm();
|
||||
$child->expects($this->once())
|
||||
->method('isValid')
|
||||
->will($this->returnValue(false));
|
||||
|
||||
$this->form->add($child);
|
||||
$this->form->submit(array());
|
||||
|
||||
$this->assertFalse($this->form->isValid());
|
||||
}
|
||||
|
||||
public function testAdd()
|
||||
{
|
||||
$child = $this->getBuilder('foo')->getForm();
|
||||
@ -564,74 +564,6 @@ class CompoundFormTest extends AbstractFormTest
|
||||
unlink($path);
|
||||
}
|
||||
|
||||
public function testSubmitPatchRequest()
|
||||
{
|
||||
if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
|
||||
$this->markTestSkipped('The "HttpFoundation" component is not available');
|
||||
}
|
||||
|
||||
$object = new \stdClass();
|
||||
$object->name = 'Bernhard';
|
||||
$object->name2 = 'Me';
|
||||
|
||||
//$values = array();
|
||||
$values = array(
|
||||
'author' => array(
|
||||
'name2' => 'Artem',
|
||||
),
|
||||
);
|
||||
|
||||
$request = new Request(array(), $values, array(), array(), array(), array(
|
||||
'REQUEST_METHOD' => 'PATCH',
|
||||
));
|
||||
|
||||
$mapper = $this->getDataMapper();
|
||||
$mapper->expects($this->any())
|
||||
->method('mapDataToForms')
|
||||
->will($this->returnCallback(function($data, $forms){
|
||||
foreach ($forms as $form) {
|
||||
$propertyPath = $form->getPropertyPath();
|
||||
|
||||
if (null !== $propertyPath) {
|
||||
$form->setData($data->{$propertyPath});
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
$mapper->expects($this->any())
|
||||
->method('mapFormsToData')
|
||||
->will($this->returnCallback(function($forms, &$data){
|
||||
foreach ($forms as $form) {
|
||||
$propertyPath = $form->getPropertyPath();
|
||||
|
||||
if (null !== $propertyPath) {
|
||||
if (!is_object($data) || $form->getData() !== $data->{$propertyPath}) {
|
||||
$data->{$propertyPath} = $form->getData();
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
/** @var Form $form */
|
||||
$form = $this->getBuilder('author', null, 'stdClass')
|
||||
->setMethod('PATCH')
|
||||
->setData($object)
|
||||
->setCompound(true)
|
||||
->setDataMapper($mapper)
|
||||
->setRequestHandler(new HttpFoundationRequestHandler())
|
||||
->getForm();
|
||||
$form->add($this->getBuilder('name')->getForm());
|
||||
$form->add($this->getBuilder('name2')->getForm());
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
$this->assertTrue($form->isValid());
|
||||
|
||||
$this->assertEquals('Bernhard', $form['name']->getData());
|
||||
$this->assertEquals('Artem', $form['name2']->getData());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider requestMethodProvider
|
||||
*/
|
||||
|
@ -64,17 +64,21 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
|
||||
* @param Boolean $synchronized
|
||||
* @return \PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
private function getForm(FormConfigInterface $config, $synchronized = true)
|
||||
private function getForm(FormConfigInterface $config, $synchronized = true, $submitted = true)
|
||||
{
|
||||
$form = $this->getMockBuilder('Symfony\Component\Form\Form')
|
||||
->setConstructorArgs(array($config))
|
||||
->setMethods(array('isSynchronized'))
|
||||
->setMethods(array('isSynchronized', 'isSubmitted'))
|
||||
->getMock();
|
||||
|
||||
$form->expects($this->any())
|
||||
->method('isSynchronized')
|
||||
->will($this->returnValue($synchronized));
|
||||
|
||||
$form->expects($this->any())
|
||||
->method('isSubmitted')
|
||||
->will($this->returnValue($submitted));
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
@ -263,6 +267,24 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
|
||||
$this->mapper->mapFormsToData(array($form), $car);
|
||||
}
|
||||
|
||||
public function testMapFormsToDataIgnoresUnsubmittedForms()
|
||||
{
|
||||
$car = new \stdClass();
|
||||
$engine = new \stdClass();
|
||||
$propertyPath = $this->getPropertyPath('engine');
|
||||
|
||||
$this->propertyAccessor->expects($this->never())
|
||||
->method('setValue');
|
||||
|
||||
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
|
||||
$config->setByReference(true);
|
||||
$config->setPropertyPath($propertyPath);
|
||||
$config->setData($engine);
|
||||
$form = $this->getForm($config, true, false);
|
||||
|
||||
$this->mapper->mapFormsToData(array($form), $car);
|
||||
}
|
||||
|
||||
public function testMapFormsToDataIgnoresEmptyData()
|
||||
{
|
||||
$car = new \stdClass();
|
||||
|
Reference in New Issue
Block a user