Skip to content
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

[Form] Ease immutable/value objects mapping #19367

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,41 @@
<?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\DataMapper;

/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class CallbackFormDataToObjectConverter implements FormDataToObjectConverterInterface
{
/**
* The callable used to map form data to an object.
*
* @var callable
*/
private $converter;

/**
* @param callable $converter
*/
public function __construct(callable $converter)
{
$this->converter = $converter;
}

/**
* {@inheritdoc}
*/
public function convertFormDataToObject(array $data, $originalData)
{
return call_user_func($this->converter, $data, $originalData);
}
}
@@ -0,0 +1,28 @@
<?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\DataMapper;

/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
interface FormDataToObjectConverterInterface
{
/**
* Convert the form data into an object.
Copy link
Contributor

@hhamon hhamon Nov 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{@inheritdoc}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interface does not extend any other interface.

*
* @param array $data Array of form data indexed by fields names.
* @param object|null $originalData Original data set in the form (after FormEvents::PRE_SET_DATA).
*
* @return object|null
*/
public function convertFormDataToObject(array $data, $originalData);
}
@@ -0,0 +1,27 @@
<?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\DataMapper;

/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
interface ObjectToFormDataConverterInterface
{
/**
* Convert given object to form data.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converts.

*
* @param object|null $object The object to map to the form.
*
* @return array The array of form data indexed by fields names.
*/
public function convertObjectToFormData($object);
}
@@ -0,0 +1,91 @@
<?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\DataMapper;

use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;

/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class SimpleObjectMapper implements DataMapperInterface
{
/**
* @var FormDataToObjectConverterInterface
*/
private $converter;

/**
* @var DataMapperInterface|null
*/
private $originalMapper;

/**
* @param FormDataToObjectConverterInterface $converter
* @param DataMapperInterface|null $originalMapper
*/
public function __construct(FormDataToObjectConverterInterface $converter, DataMapperInterface $originalMapper = null)
{
$this->converter = $converter;
$this->originalMapper = $originalMapper;
}

/**
* {@inheritdoc}
*/
public function mapDataToForms($data, $forms)
{
// Fallback to original mapper instance or default to "PropertyPathMapper"
// mapper implementation if not an "ObjectToFormDataConverterInterface" instance:
if (!$this->converter instanceof ObjectToFormDataConverterInterface) {
$propertyPathMapper = $this->originalMapper ?: new PropertyPathMapper();
$propertyPathMapper->mapDataToForms($data, $forms);

return;
}

if (!is_object($data) && null !== $data) {
throw new UnexpectedTypeException($data, 'object or null');
}

$data = $this->converter->convertObjectToFormData($data);

if (!is_array($data)) {
throw new UnexpectedTypeException($data, 'array');
}

foreach ($forms as $form) {
$config = $form->getConfig();

if ($config->getMapped() && isset($data[$form->getName()])) {
$form->setData($data[$form->getName()]);

continue;
}

$form->setData($config->getData());
}
}

/**
* {@inheritdoc}
*/
public function mapFormsToData($forms, &$data)
{
$fieldsData = array();
foreach ($forms as $form) {
$fieldsData[$form->getName()] = $form->getData();
}

$data = $this->converter->convertFormDataToObject($fieldsData, $data);
}
}
35 changes: 34 additions & 1 deletion src/Symfony/Component/Form/Extension/Core/Type/FormType.php
Expand Up @@ -11,6 +11,9 @@

namespace Symfony\Component\Form\Extension\Core\Type;

use Symfony\Component\Form\Extension\Core\DataMapper\CallbackFormDataToObjectConverter;
use Symfony\Component\Form\Extension\Core\DataMapper\FormDataToObjectConverterInterface;
use Symfony\Component\Form\Extension\Core\DataMapper\SimpleObjectMapper;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
Expand Down Expand Up @@ -43,6 +46,15 @@ public function buildForm(FormBuilderInterface $builder, array $options)

$isDataOptionSet = array_key_exists('data', $options);

$dataMapper = null;
if ($options['compound']) {
$dataMapper = new PropertyPathMapper($this->propertyAccessor);

if (isset($options['simple_object_mapper'])) {
$dataMapper = new SimpleObjectMapper($options['simple_object_mapper'], $dataMapper);
}
}

$builder
->setRequired($options['required'])
->setErrorBubbling($options['error_bubbling'])
Expand All @@ -54,7 +66,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
->setCompound($options['compound'])
->setData($isDataOptionSet ? $options['data'] : null)
->setDataLocked($isDataOptionSet)
->setDataMapper($options['compound'] ? new PropertyPathMapper($this->propertyAccessor) : null)
->setDataMapper($dataMapper)
->setMethod($options['method'])
->setAction($options['action']);

Expand Down Expand Up @@ -133,6 +145,11 @@ public function configureOptions(OptionsResolver $resolver)

if (null !== $class) {
return function (FormInterface $form) use ($class) {
// If the "SimpleObjectMapper" is used, the "empty_data" option value should be null:
if ($form->getConfig()->getDataMapper() instanceof SimpleObjectMapper) {
return;
}

return $form->isEmpty() && !$form->isRequired() ? null : new $class();
};
}
Expand All @@ -155,6 +172,18 @@ public function configureOptions(OptionsResolver $resolver)
return $options['compound'];
};

$simpleObjectMapperNormalizer = function (Options $options, $value) {
if (null === $value) {
return;
}

if (!$value instanceof FormDataToObjectConverterInterface && is_callable($value)) {
return new CallbackFormDataToObjectConverter($value);
}

return $value;
};

// If data is given, the form is locked to that data
// (independent of its value)
$resolver->setDefined(array(
Expand All @@ -180,10 +209,14 @@ public function configureOptions(OptionsResolver $resolver)
'attr' => array(),
'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.',
'upload_max_size_message' => $uploadMaxSizeMessage, // internal
'simple_object_mapper' => null,
));

$resolver->setNormalizer('simple_object_mapper', $simpleObjectMapperNormalizer);

$resolver->setAllowedTypes('label_attr', 'array');
$resolver->setAllowedTypes('upload_max_size_message', array('callable'));
$resolver->setAllowedTypes('simple_object_mapper', array(FormDataToObjectConverterInterface::class, 'null', 'callable'));
}

/**
Expand Down
@@ -0,0 +1,32 @@
<?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\Tests\Extension\Core\DataMapper;

use Symfony\Component\Form\Extension\Core\DataMapper\CallbackFormDataToObjectConverter;

class CallbackFormDataToObjectConverterTest extends \PHPUnit_Framework_TestCase
{
public function testConvertFormDataToObject()
{
$data = array('amount' => 15.0, 'currency' => 'EUR');
$originalData = (object) $data;

$converter = new CallbackFormDataToObjectConverter(function ($arg1, $arg2) use ($data, $originalData) {
$this->assertSame($data, $arg1);
$this->assertSame($originalData, $arg2);

return 'converted';
});

$this->assertSame('converted', $converter->convertFormDataToObject($data, $originalData));
}
}