-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[POC] [Form] Support get/set accessors for form fields #37614
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
Changes from all commits
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 |
---|---|---|
@@ -0,0 +1,98 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Form\Extension\Core\DataMapper; | ||
|
||
use Symfony\Component\Form\DataMapperInterface; | ||
use Symfony\Component\Form\Exception\ExceptionInterface; | ||
use Symfony\Component\Form\Exception\UnexpectedTypeException; | ||
use Symfony\Component\Form\FormError; | ||
use TypeError; | ||
|
||
class AccessorMapper implements DataMapperInterface | ||
{ | ||
private $get; | ||
private $set; | ||
private $fallbackMapper; | ||
|
||
public function __construct(?\Closure $get, ?\Closure $set, DataMapperInterface $fallbackMapper) | ||
{ | ||
$this->get = $get; | ||
$this->set = $set; | ||
$this->fallbackMapper = $fallbackMapper; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function mapDataToForms($data, iterable $forms) | ||
{ | ||
$empty = null === $data || [] === $data; | ||
|
||
if (!$empty && !\is_array($data) && !\is_object($data)) { | ||
throw new UnexpectedTypeException($data, 'object, array or empty'); | ||
} | ||
|
||
if (!$this->get) { | ||
$this->fallbackMapper->mapDataToForms($data, $forms); | ||
return; | ||
} | ||
|
||
foreach ($forms as $form) { | ||
$config = $form->getConfig(); | ||
|
||
if (!$empty && $config->getMapped()) { | ||
$form->setData($this->getPropertyValue($data)); | ||
} else { | ||
$form->setData($config->getData()); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function mapFormsToData(iterable $forms, &$data) | ||
{ | ||
if (null === $data) { | ||
return; | ||
} | ||
|
||
if (!\is_array($data) && !\is_object($data)) { | ||
throw new UnexpectedTypeException($data, 'object, array or empty'); | ||
} | ||
|
||
if (!$this->set) { | ||
$this->fallbackMapper->mapFormsToData($forms, $data); | ||
return; | ||
} | ||
|
||
foreach ($forms as $form) { | ||
$config = $form->getConfig(); | ||
|
||
// 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 !== $this->set && $config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled()) { | ||
try { | ||
$returnValue = ($this->set)($data, $form->getData()); | ||
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. Given a compound form with two fields (let's say 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. Basically, I expected something like this |
||
} catch (ExceptionInterface | TypeError $e) { | ||
$form->addError(new FormError($e->getMessage())); | ||
continue; | ||
} | ||
|
||
$type = is_object($returnValue) ? get_class($returnValue) : gettype($returnValue); | ||
|
||
if ( | ||
(is_scalar($data) && gettype($data) === $type) | ||
|| (is_array($data) && is_array($returnValue)) | ||
|| (is_object($data) && $returnValue instanceof $type)) { | ||
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. [Immutability] We could also apply the same approach as data mappers but on the closure $builder->add('name', null, [
'set' => function(Category &$category, string $name) {
$category = $category->rename($name);
},
]); 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. Interesting idea. Passing the argument by reference also makes it clearer that you are always modifying the data directly, apart from being consistent with the data mapper API. |
||
$data = $returnValue; | ||
} | ||
} | ||
} | ||
} | ||
|
||
private function getPropertyValue($data) | ||
{ | ||
return $this->get ? ($this->get)($data) : null; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?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\Extension\Core\Type; | ||
|
||
use Closure; | ||
use Symfony\Component\Form\AbstractTypeExtension; | ||
use Symfony\Component\Form\Extension\Core\DataMapper\AccessorMapper; | ||
use Symfony\Component\Form\FormBuilderInterface; | ||
use Symfony\Component\OptionsResolver\OptionsResolver; | ||
|
||
class AccessorMapperExtension extends AbstractTypeExtension | ||
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. Missing service registration in |
||
{ | ||
public function buildForm(FormBuilderInterface $builder, array $options) | ||
{ | ||
if (!$options['get'] && !$options['set']) { | ||
return; | ||
} | ||
|
||
if (!$dataMapper = $builder->getDataMapper()) { | ||
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. By default, there is no data mapper bound to single form fields (see 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 chicken and egg problem, if the parent (compound) form doesn't configure any accessor option then 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. Ah, yes. I'll add some functional tests to cover this as well. |
||
return; | ||
} | ||
|
||
$builder->setDataMapper(new AccessorMapper($options['get'], $options['set'], $dataMapper)); | ||
} | ||
|
||
public function configureOptions(OptionsResolver $resolver) | ||
{ | ||
$resolver->setDefaults([ | ||
'get' => null, | ||
'set' => null, | ||
]); | ||
|
||
$resolver->setAllowedTypes('get', ['null', Closure::class]); | ||
$resolver->setAllowedTypes('set', ['null', Closure::class]); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public static function getExtendedTypes(): iterable | ||
{ | ||
return [FormType::class]; | ||
} | ||
} |
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.
It shouldn't be possible for
$this->set
to benull
as you checked it a few lines earlier (to fallback on the old datamapper).Unless I read something wrong ?
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.
You are correct. I'll update this.