From 8affcdd73c415740dead9ab6b409d92b030478c8 Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Fri, 15 Jul 2016 15:25:41 +0200 Subject: [PATCH] [Form] Ease immutable/value objects mapping by adding a new "simple_object_mapper" FormType option. --- .../CallbackFormDataToObjectConverter.php | 41 +++ .../FormDataToObjectConverterInterface.php | 28 ++ .../ObjectToFormDataConverterInterface.php | 27 ++ .../Core/DataMapper/SimpleObjectMapper.php | 91 +++++++ .../Form/Extension/Core/Type/FormType.php | 35 ++- .../CallbackFormDataToObjectConverterTest.php | 32 +++ .../DataMapper/SimpleObjectMapperTest.php | 253 ++++++++++++++++++ .../Extension/Core/Type/FormTypeTest.php | 100 +++++++ .../Component/Form/Tests/Fixtures/Money.php | 37 +++ .../Form/Tests/Fixtures/MoneyType.php | 36 +++ .../Tests/Fixtures/MoneyTypeConverter.php | 32 +++ 11 files changed, 711 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Form/Extension/Core/DataMapper/CallbackFormDataToObjectConverter.php create mode 100644 src/Symfony/Component/Form/Extension/Core/DataMapper/FormDataToObjectConverterInterface.php create mode 100644 src/Symfony/Component/Form/Extension/Core/DataMapper/ObjectToFormDataConverterInterface.php create mode 100644 src/Symfony/Component/Form/Extension/Core/DataMapper/SimpleObjectMapper.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/CallbackFormDataToObjectConverterTest.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/SimpleObjectMapperTest.php create mode 100644 src/Symfony/Component/Form/Tests/Fixtures/Money.php create mode 100644 src/Symfony/Component/Form/Tests/Fixtures/MoneyType.php create mode 100644 src/Symfony/Component/Form/Tests/Fixtures/MoneyTypeConverter.php diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/CallbackFormDataToObjectConverter.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/CallbackFormDataToObjectConverter.php new file mode 100644 index 0000000000000..a4428c1dc5ca9 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/CallbackFormDataToObjectConverter.php @@ -0,0 +1,41 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/FormDataToObjectConverterInterface.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/FormDataToObjectConverterInterface.php new file mode 100644 index 0000000000000..bf25fdfa991a0 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/FormDataToObjectConverterInterface.php @@ -0,0 +1,28 @@ + + * + * 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 + */ +interface FormDataToObjectConverterInterface +{ + /** + * Convert the form data into an object. + * + * @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); +} diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/ObjectToFormDataConverterInterface.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/ObjectToFormDataConverterInterface.php new file mode 100644 index 0000000000000..a25732c2b4441 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/ObjectToFormDataConverterInterface.php @@ -0,0 +1,27 @@ + + * + * 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 + */ +interface ObjectToFormDataConverterInterface +{ + /** + * Convert given object to form data. + * + * @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); +} diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/SimpleObjectMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/SimpleObjectMapper.php new file mode 100644 index 0000000000000..f7730200d5dc3 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/SimpleObjectMapper.php @@ -0,0 +1,91 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php index 86894f1adc6bf..3c224c3b64cc2 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php @@ -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; @@ -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']) @@ -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']); @@ -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(); }; } @@ -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( @@ -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')); } /** diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/CallbackFormDataToObjectConverterTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/CallbackFormDataToObjectConverterTest.php new file mode 100644 index 0000000000000..c59b3ac57a659 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/CallbackFormDataToObjectConverterTest.php @@ -0,0 +1,32 @@ + + * + * 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)); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/SimpleObjectMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/SimpleObjectMapperTest.php new file mode 100644 index 0000000000000..f02ff13f20670 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/SimpleObjectMapperTest.php @@ -0,0 +1,253 @@ + + * + * 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\DataMapperInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Core\DataMapper\FormDataToObjectConverterInterface; +use Symfony\Component\Form\Extension\Core\DataMapper\ObjectToFormDataConverterInterface; +use Symfony\Component\Form\Extension\Core\DataMapper\SimpleObjectMapper; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\Forms; +use Symfony\Component\Form\Tests\Fixtures\Money; +use Symfony\Component\Form\Tests\Fixtures\MoneyTypeConverter; + +class SimpleObjectMapperTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var FormFactoryInterface + */ + protected $factory; + + protected function setUp() + { + $this->factory = Forms::createFormFactoryBuilder()->getFormFactory(); + } + + public function testMapDataToFormsUsesOriginalMapper() + { + $converter = $this->getMockBuilder(FormDataToObjectConverterInterface::class)->getMock(); + + $originalMapper = $this->getMockBuilder(DataMapperInterface::class)->getMock(); + $originalMapper->expects($this->once())->method('mapDataToForms'); + + $simpleObjectMapper = new SimpleObjectMapper($converter, $originalMapper); + $simpleObjectMapper->mapDataToForms(new \stdClass(), array()); + } + + public function testMapDataToFormsUsesConverterOnObjectToFormDataConverterInterfaceInstance() + { + $converter = $this->getMockBuilder(ConverterStub::class)->getMock(); + $converter->expects($this->once())->method('convertObjectToFormData')->willReturn(array()); + + $originalMapper = $this->getMockBuilder(DataMapperInterface::class)->getMock(); + $originalMapper->expects($this->never())->method('mapDataToForms'); + + $simpleObjectMapper = new SimpleObjectMapper($converter, $originalMapper); + $simpleObjectMapper->mapDataToForms(new \stdClass(), array()); + } + + public function testItProperlyMapsObject() + { + $money = new Money(20.5, 'EUR'); + + $simpleObjectMapper = new SimpleObjectMapper(new FormDataToMoneyConverter($money)); + + /** @var FormInterface[] $forms */ + $forms = array( + 'amount' => $this->factory->createNamed('amount', NumberType::class), + 'currency' => $this->factory->createNamed('currency'), + ); + + $simpleObjectMapper->mapDataToForms($money, $forms); + + $this->assertSame(20.5, $forms['amount']->getData()); + $this->assertSame('EUR', $forms['currency']->getData()); + + $newMoney = $money; + $forms['amount']->setData(15.0); + $forms['currency']->setData('USD'); + $simpleObjectMapper->mapFormsToData($forms, $newMoney); + + $this->assertNotSame($money, $newMoney); + $this->assertInstanceOf(Money::class, $newMoney); + $this->assertSame(15.0, $newMoney->getAmount()); + $this->assertSame('USD', $newMoney->getCurrency()); + } + + public function testSettingSimpleObjectMapperOnForm() + { + $money = new Money(20.5, 'EUR'); + + $simpleObjectMapper = new SimpleObjectMapper(new FormDataToMoneyConverter($money)); + + $form = $this->factory->createBuilder(FormType::class, $money, array('data_class' => Money::class)) + ->add('amount', NumberType::class) + ->add('currency') + ->setDataMapper($simpleObjectMapper) + ->getForm() + ; + + $form->submit(array('amount' => 15.0, 'currency' => 'USD')); + + $newMoney = $form->getData(); + + $this->assertNotSame($money, $newMoney); + $this->assertInstanceOf(Money::class, $newMoney); + $this->assertSame(15.0, $newMoney->getAmount()); + $this->assertSame('USD', $newMoney->getCurrency()); + } + + public function testItProperlyMapsObjectWithObjectToFormDataConverter() + { + $media = new Book('foo'); + + $simpleObjectMapper = new SimpleObjectMapper(new MediaConverter()); + + /** @var FormInterface[] $forms */ + $forms = array( + 'author' => $this->factory->createNamed('author'), + 'mediaType' => $this->factory->createNamed('mediaType'), + ); + + $simpleObjectMapper->mapDataToForms($media, $forms); + + $this->assertSame('foo', $forms['author']->getData()); + $this->assertSame('book', $forms['mediaType']->getData()); + + $newMedia = $media; + $forms['author']->setData('bar'); + $forms['mediaType']->setData('movie'); + $simpleObjectMapper->mapFormsToData($forms, $newMedia); + + $this->assertNotSame($media, $newMedia); + $this->assertInstanceOf(Movie::class, $newMedia); + $this->assertSame('bar', $newMedia->getAuthor()); + } + + public function testSettingSimpleObjectOnFormWithObjectToFormDataConverter() + { + $media = new Book('foo'); + + $simpleObjectMapper = new SimpleObjectMapper(new MediaConverter()); + + $form = $this->factory->createBuilder(FormType::class, $media, array('data_class' => Media::class)) + ->add('author') + ->add('mediaType') + ->setDataMapper($simpleObjectMapper) + ->getForm() + ; + + $this->assertSame('foo', $form->get('author')->getData()); + $this->assertSame('book', $form->get('mediaType')->getData()); + + $form->submit(array('author' => 'bar', 'mediaType' => 'movie')); + + $newMedia = $form->getData(); + + $this->assertNotSame($media, $newMedia); + $this->assertInstanceOf(Movie::class, $newMedia); + $this->assertSame('bar', $newMedia->getAuthor()); + } +} + +class FormDataToMoneyConverter extends \PHPUnit_Framework_TestCase implements FormDataToObjectConverterInterface +{ + private $originalData; + + public function __construct($originalData) + { + $this->originalData = $originalData; + } + + public function convertFormDataToObject(array $data, $originalData) + { + $this->assertSame($this->originalData, $originalData); + + $converter = new MoneyTypeConverter(); + + return $converter->convertFormDataToObject($data, $originalData); + } +} + +interface ConverterStub extends FormDataToObjectConverterInterface, ObjectToFormDataConverterInterface +{ +} + +class MediaConverter implements FormDataToObjectConverterInterface, ObjectToFormDataConverterInterface +{ + public function convertFormDataToObject(array $data, $originalData = null) + { + $author = $data['author']; + + switch ($data['mediaType']) { + case 'movie': + return new Movie($author); + case 'book': + return new Book($author); + default: + throw new TransformationFailedException(); + } + } + + /** + * {@inheritdoc} + * + * @param Media|null $object + */ + public function convertObjectToFormData($object) + { + if (null === $object) { + return array(); + } + + $mediaTypeByClass = array( + Movie::class => 'movie', + Book::class => 'book', + ); + + if (!isset($mediaTypeByClass[get_class($object)])) { + throw new TransformationFailedException(); + } + + return array( + 'mediaType' => $mediaTypeByClass[get_class($object)], + 'author' => $object->getAuthor(), + ); + } +} + +abstract class Media +{ + private $author; + + public function __construct($author) + { + $this->author = $author; + } + + public function getAuthor() + { + return $this->author; + } +} + +class Movie extends Media +{ +} + +class Book extends Media +{ +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php index 0cde03e51f8c8..f55c006110a7e 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php @@ -11,11 +11,18 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\Tests\Fixtures\MoneyType; +use Symfony\Component\Form\Tests\Fixtures\MoneyTypeConverter; use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Extension\Core\DataMapper\CallbackFormDataToObjectConverter; +use Symfony\Component\Form\Extension\Core\DataMapper\SimpleObjectMapper; +use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Tests\Fixtures\Author; use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer; use Symfony\Component\Form\FormError; +use Symfony\Component\Form\Tests\Fixtures\Money; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; class FormTest_AuthorWithoutRefSetter { @@ -629,6 +636,99 @@ public function testPassZeroLabelToView() $this->assertSame('0', $view->vars['label']); } + public function provideSimpleObjectMapperOption() + { + return array( + array(false, true), + array('foo', true), + array(function (array $data, $originalData) {}, false), + array(new CallbackFormDataToObjectConverter(function (array $data, $originalData) {}), false), + array(null, false), + ); + } + + /** + * @dataProvider provideSimpleObjectMapperOption + */ + public function testSimpleObjectMapperOptionExpectations($value, $expectsException = false) + { + if ($expectsException) { + $this->setExpectedException(InvalidOptionsException::class); + } + + $this->factory->create(FormType::class, null, array( + 'simple_object_mapper' => $value, + )); + } + + public function testSimpleObjectMapperOptionSetsDataMapper() + { + $form = $this->factory->create(FormType::class, null, array( + 'simple_object_mapper' => function (array $data, $originalData) {}, + )); + + $this->assertInstanceOf(SimpleObjectMapper::class, $form->getConfig()->getDataMapper()); + } + + public function testEmptyDataCallableReturnsNullWithSimpleObjectMapper() + { + $form = $this->factory->create(FormType::class, null, array( + 'data_class' => \stdClass::class, + 'simple_object_mapper' => function (array $data, $originalData) {}, + )); + + $this->assertNull(call_user_func($form->getConfig()->getEmptyData(), $form)); + } + + public function testSimpleObjectMapperOptionProperlyMapsObject() + { + $money = new Money(20.5, 'EUR'); + + $form = $this->factory->create(MoneyType::class, $money, array( + 'simple_object_mapper' => function (array $data, $originalData) use ($money) { + $this->assertSame($money, $originalData); + + $converter = new MoneyTypeConverter(); + + return $converter->convertFormDataToObject($data, $originalData); + }, )) + ; + + $this->assertSame($money->getAmount(), $form->get('amount')->getData()); + $this->assertSame($money->getCurrency(), $form->get('currency')->getData()); + + $form->submit(array('amount' => 15.0, 'currency' => 'USD')); + + $newMoney = $form->getData(); + + $this->assertNotSame($money, $newMoney); + $this->assertInstanceOf(Money::class, $newMoney); + $this->assertSame(15.0, $newMoney->getAmount()); + $this->assertSame('USD', $newMoney->getCurrency()); + } + + public function provideTestSimpleObjectMapperOptionData() + { + return array( + array(new Money(20.5, 'EUR'), array('amount' => 15.0, 'currency' => 'USD'), new Money(15.0, 'USD')), + array(new Money(20.5, 'EUR'), array(), null), + array(null, array('amount' => 15.0, 'currency' => 'USD'), new Money(15.0, 'USD')), + array(null, array(), null), + ); + } + + /** + * @dataProvider provideTestSimpleObjectMapperOptionData + */ + public function testSimpleObjectMapperOption($initialData, $submittedData, $expectedData) + { + $form = $this->factory->create(MoneyType::class, $initialData); + + $form->submit($submittedData); + + $this->assertEquals($expectedData, $form->getData()); + } + protected function getTestedType() { return 'Symfony\Component\Form\Extension\Core\Type\FormType'; diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Money.php b/src/Symfony/Component/Form/Tests/Fixtures/Money.php new file mode 100644 index 0000000000000..14e4f813a1eda --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/Money.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Fixtures; + +class Money +{ + /** @var float */ + private $amount; + + /** @var string */ + private $currency; + + public function __construct($amount, $currency) + { + $this->amount = $amount; + $this->currency = $currency; + } + + public function getAmount() + { + return $this->amount; + } + + public function getCurrency() + { + return $this->currency; + } +} diff --git a/src/Symfony/Component/Form/Tests/Fixtures/MoneyType.php b/src/Symfony/Component/Form/Tests/Fixtures/MoneyType.php new file mode 100644 index 0000000000000..d1160d73e9fb0 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/MoneyType.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Fixtures; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class MoneyType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('amount', NumberType::class) + ->add('currency') + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'data_class' => Money::class, + 'simple_object_mapper' => new MoneyTypeConverter(), + )); + } +} diff --git a/src/Symfony/Component/Form/Tests/Fixtures/MoneyTypeConverter.php b/src/Symfony/Component/Form/Tests/Fixtures/MoneyTypeConverter.php new file mode 100644 index 0000000000000..d58280724e240 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/MoneyTypeConverter.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Fixtures; + +use Symfony\Component\Form\Extension\Core\DataMapper\FormDataToObjectConverterInterface; + +class MoneyTypeConverter implements FormDataToObjectConverterInterface +{ + /** + * {@inheritdoc} + * + * @param Money|null $originalData + */ + public function convertFormDataToObject(array $data, $originalData) + { + // Logic to determine if the result should be considered null according to form fields data. + if (null === $data['amount'] && null === $data['currency']) { + return; + } + + return new Money($data['amount'], $data['currency']); + } +}