-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Form] Fix form value merging involving file upload, collection & checkbox #54324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 5.4
Are you sure you want to change the base?
Changes from 3 commits
597b6a3
f943adb
fdc3e01
79fdbaa
0c9b3d7
f3b5efd
55deed0
aec4b56
21fbb71
60008f4
30c95b8
ac24610
1cf2c5f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -355,6 +355,53 @@ public function testMergeZeroIndexedCollection() | |
$this->assertNotNull($itemsForm->get('0')->get('file')); | ||
} | ||
|
||
public function testMergePartialDataFromCollection() | ||
{ | ||
$form = $this->createForm('root', 'POST', true); | ||
$form->add('items', CollectionType::class, [ | ||
'entry_type' => ItemFileType::class, | ||
'allow_add' => true, | ||
]); | ||
|
||
$file = $this->getUploadedFile(); | ||
$file2 = $this->getUploadedFile(); | ||
|
||
$this->setRequestData('POST', [ | ||
'root' => [ | ||
'items' => [ | ||
1 => [ | ||
'item' => 'test', | ||
], | ||
], | ||
], | ||
], [ | ||
'root' => [ | ||
'items' => [ | ||
0 => [ | ||
'file' => $file, | ||
], | ||
1 => [ | ||
'file' => $file2, | ||
], | ||
], | ||
], | ||
]); | ||
|
||
$this->requestHandler->handleRequest($form, $this->request); | ||
|
||
$itemsForm = $form->get('items'); | ||
$data = $itemsForm->getData(); | ||
$this->assertTrue($form->isSubmitted()); | ||
$this->assertTrue($form->isValid()); | ||
|
||
$this->assertCount(2, $data); | ||
$this->assertArrayHasKey(0, $data); | ||
$this->assertArrayHasKey(1, $data); | ||
|
||
$this->assertEquals('test', $itemsForm->get('1')->get('item')->getData()); | ||
$this->assertNotNull($itemsForm->get('0')->get('file')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't this assert the form data is not null instead of asserting that the Form instance is not null ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw, |
||
} | ||
|
||
/** | ||
* @dataProvider methodExceptGetProvider | ||
*/ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,8 +30,6 @@ private function __construct() | |
* a form and needs to be consistent. PHP keyword `empty` cannot | ||
* be used as it also considers 0 and "0" to be empty. | ||
* | ||
* @param mixed $data | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's keep this one There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fabbot insists that I remove it. I reverted the changes, but it won't pass the test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the change has not been reverted |
||
* | ||
* @return bool | ||
*/ | ||
public static function isEmpty($data) | ||
|
@@ -43,30 +41,10 @@ public static function isEmpty($data) | |
} | ||
|
||
/** | ||
* Recursively replaces or appends elements of the first array with elements | ||
* of second array. If the key is an integer, the values will be appended to | ||
* the new array; otherwise, the value from the second array will replace | ||
* the one from the first array. | ||
* Merges query string or post parameters with uploaded files. | ||
*/ | ||
public static function mergeParamsAndFiles(array $params, array $files): array | ||
{ | ||
$isFilesList = array_is_list($files); | ||
|
||
foreach ($params as $key => $value) { | ||
if (\is_array($value) && \is_array($files[$key] ?? null)) { | ||
$params[$key] = self::mergeParamsAndFiles($value, $files[$key]); | ||
unset($files[$key]); | ||
} | ||
} | ||
|
||
if (!$isFilesList) { | ||
return array_replace($params, $files); | ||
} | ||
|
||
foreach ($files as $value) { | ||
$params[] = $value; | ||
} | ||
|
||
return $params; | ||
return (new ParamFilesMerger([], $params, $files))->getResult(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Form\Util; | ||
|
||
use Symfony\Component\HttpFoundation\File\UploadedFile; | ||
|
||
/** | ||
* @author Priyadi Iman Nurcahyo <priyadi@rekalogika.com> | ||
*/ | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
class ParamFilesMerger | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of introducing this new internal class the code should be moved into private methods in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The class represents a node in the tree. It is used by itself to traverse the entire tree. The task will be very complicated without a dedicated class. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have sent a PR against your fork which inlines the methods from the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have merged your PR |
||
{ | ||
private array $path; | ||
private array $params; | ||
private array $files; | ||
|
||
/** | ||
* @param array $path The path to the current element, empty means the root | ||
* @param array $params The parameters | ||
* @param array $files The files | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
public function __construct(array $path, array $params, array $files) | ||
{ | ||
$this->path = $path; | ||
$this->params = $params; | ||
$this->files = $files; | ||
} | ||
|
||
public function getResult() | ||
{ | ||
$paramsValue = $this->getParamsValue(); | ||
$filesValue = $this->getFilesValue(); | ||
|
||
if (null === $paramsValue) { | ||
if (null === $filesValue) { | ||
return null; | ||
} elseif (self::isFileUpload($filesValue)) { | ||
return $filesValue; | ||
} elseif (\is_array($filesValue)) { | ||
return $filesValue; | ||
} else { // $filesValue has a non-array value | ||
return $filesValue; | ||
} | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} elseif (\is_array($paramsValue)) { | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (null === $filesValue) { | ||
return $paramsValue; | ||
} elseif (self::isFileUpload($filesValue)) { | ||
return $filesValue; // if the array is a file upload field, it has the precedence | ||
} elseif (\is_array($filesValue)) { | ||
return $this->getResultArray($paramsValue, $filesValue); | ||
} else { // $filesValue has a non-array value | ||
return $paramsValue; // params has the precedence | ||
} | ||
} else { // $paramsValue has a non-array value | ||
if (null === $filesValue) { | ||
return $paramsValue; | ||
} elseif (self::isFileUpload($filesValue)) { | ||
return $filesValue; // if the array is a file upload field, it has the precedence | ||
} elseif (\is_array($filesValue)) { | ||
return $paramsValue; // params has the precedence | ||
} else { // $filesValue has a non-array value | ||
return $paramsValue; // params has the precedence | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
|
||
/** | ||
* @return bool | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
private static function isFileUpload($value) | ||
{ | ||
if ($value instanceof UploadedFile) { | ||
return true; | ||
} | ||
|
||
if (!\is_array($value)) { | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return false; | ||
} | ||
|
||
$keys = array_keys($value); | ||
sort($keys); | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return $keys === ['error', 'name', 'size', 'tmp_name', 'type']; | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* @param array $array | ||
* | ||
* @return bool | ||
*/ | ||
private static function doesNotContainArrayOrFileUpload($array) | ||
{ | ||
foreach ($array as $value) { | ||
if ( | ||
\is_array($value) | ||
&& !self::isFileUpload($value) | ||
) { | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* @return array | ||
*/ | ||
private function getResultArray(array $paramsValue, array $filesValue) | ||
{ | ||
// if both are lists and both does not contains array, then merge them and return | ||
if ( | ||
array_is_list($paramsValue) | ||
&& self::doesNotContainArrayOrFileUpload($paramsValue) | ||
&& array_is_list($filesValue) | ||
&& self::doesNotContainArrayOrFileUpload($filesValue) | ||
) { | ||
return array_merge($paramsValue, $filesValue); | ||
} | ||
|
||
// heuristics to preserve order, the bigger array wins | ||
if (\count($filesValue) > \count($paramsValue)) { | ||
$keys = array_unique(array_merge(array_keys($filesValue), array_keys($paramsValue))); | ||
} else { | ||
$keys = array_unique(array_merge(array_keys($paramsValue), array_keys($filesValue))); | ||
} | ||
|
||
$result = []; | ||
|
||
foreach ($keys as $key) { | ||
$path = $this->path; | ||
$path[] = $key; | ||
|
||
$node = new self($path, $this->params, $this->files); | ||
|
||
$result[$key] = $node->getResult(); | ||
} | ||
|
||
return $result; | ||
} | ||
|
||
/** | ||
* Gets the value of the current element in the params according to the path. | ||
*/ | ||
private function getParamsValue() | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
$params = $this->params; | ||
|
||
foreach ($this->path as $key) { | ||
$params = $params[$key] ?? null; | ||
|
||
if (null === $params) { | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return null; | ||
} | ||
} | ||
|
||
return $params; | ||
} | ||
|
||
/** | ||
* Gets the value of the current element in the files according to the path. | ||
*/ | ||
private function getFilesValue() | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
$files = $this->files; | ||
|
||
foreach ($this->path as $key) { | ||
$files = $files[$key] ?? null; | ||
|
||
if (null === $files) { | ||
priyadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return null; | ||
} | ||
} | ||
|
||
return $files; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this misses assertions on
$itemsForm->get('0')->get('name')
and$itemsForm->get('1')->get('file')