elao/form-simple-object-mapper

此包已废弃,不再维护。作者建议使用 sensiolabs-de/rich-model-forms-bundle 包代替。

轻松将不可变/值对象映射到 Symfony 表单

v1.1.0 2018-04-03 12:10 UTC

This package is auto-updated.

Last update: 2022-02-01 13:02:56 UTC


README

⚠️ 此包已废弃,推荐使用 https://github.com/sensiolabs-de/rich-model-forms-bundle,并使用 factory 选项和可调用的对象。
更多信息请参考 https://github.com/sensiolabs-de/rich-model-forms-bundle/blob/master/docs/factory_value_object.md#initializing-objects

Symfony Form Simple Object Mapper

Latest Stable Version Total Downloads Monthly Downloads Build Status Coveralls Scrutinizer Code Quality Symfony php

此库旨在简化基于 Bernhard Schussek (Webmozart) 的博客文章 "Value Objects in Symfony Forms" 的不可变或值对象与 Symfony Form 组件的映射,直到 https://github.com/symfony/symfony/pull/19367 的决定。

目录

安装

$ composer require elao/form-simple-object-mapper

使用 Symfony 全栈框架

<?php
// app/AppKernel.php

// ...
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = [
            // ...
            new Elao\FormSimpleObjectMapper\Bridge\Symfony\Bundle\ElaoFormSimpleObjectMapperBundle(),
        ];
    }

    // ...
}

仅使用 Symfony Form 组件

在表单工厂中使用 FormFactoryBuilder 注册类型扩展

<?php

use Elao\FormSimpleObjectMapper\Type\Extension\SimpleObjectMapperTypeExtension;
use Symfony\Component\Form\Forms;
 
$builder = Forms::createFormFactoryBuilder();
$builder->addTypeExtension(new SimpleObjectMapperTypeExtension());
$factory = $builder->getFormFactory();

使用

该库旨在提供一个解决方案,无需修改您的领域或应用程序模型,以满足 Symfony Form 组件的要求。
您的类的结构设计不应因基础设施约束(您在项目中使用的库)而被破坏

这尤其适用于使用命令总线的情况,例如 thephpleague/tactician

想象一个简单的 AddItemToCartCommand 命令

<?php

namespace Acme\Application\Cart\Command;

class AddItemToCartCommand
{
    /** @var string */
    private $reference;

    /** @var int */
    private $quantity;

    public function __construct($reference, $quantity)
    {
        $this->reference = $reference;
        $this->quantity = $quantity;
    }

    public function getReference()
    {
        return $this->reference;
    }
    
    public function getQuantity()
    {
        return $this->quantity;
    }
}

您的控制器将看起来像

<?php

class CartController extends Controller
{
    //...

    public function addItemAction(Request $request)
    {
        $builder = $this
            ->createFormBuilder()
            ->add('reference', HiddenType::class)
            ->add('quantity', IntegerType::class)
        ;
        
        $form = $builder->getForm();

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $data = $form->getData();
            
            $command = new AddItemToCartCommand($data['reference'], $data['quantity']);
            
            $this->getCommandBus()->handle($command);

            return $this->redirect(/*...*/);
        }

        return $this->render(':cart:add_item.html.twig', [
            'form' => $form->createView(),
        ]);
    }
    
    //...
}

尽管这工作得很好,但您被迫从表单数据创建 AddItemToCartCommand 对象,并且验证是在原始表单数据上而不是您的对象上处理的。您也没有在表单内部操作对象,这在处理更复杂的表单和表单事件时可能是一个问题。

因为表单负责将请求映射到您的应用程序中具有意义的对象,所以从请求到表单组件委托我们命令的创建是有意义的。
因此,您将创建一个类似于以下形式的表单类型

<?php

class AddItemToCartType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('reference', HiddenType::class)
            ->add('quantity', IntegerType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => AddItemToCartCommand::class,
            'empty_data' => new AddItemToCart('', 1),
        ]);
    }
}

然后是您的新控制器

<?php

class CartController extends Controller
{
    //...

    public function addItemAction(Request $request)
    {
        $form = $this->createForm(AddItemToCartType::class);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $command = $form->getData();
            
            $this->getCommandBus()->handle($command);

            return $this->redirect(/*...*/);
        }

        return $this->render(':cart:add_item.html.twig', [
            'form' => $form->createView(),
        ]);
    }
    
    //...
}

尽管它创建表单时工作得很好,但在提交时它不能原生地工作。

在类 "AddItemToCartCommand" 中,既不存在属性 "reference",也没有 "setReference()"、"__set()" 或 "__call()" 这些方法具有公共访问权限。

这是由以下事实解释的:表单组件默认使用 PropertyPathMapper,它尝试使用不同的方式访问和设置对象属性,例如公共获取器/设置器或公共属性(它内部使用 Symfony PropertyAccess 组件)。

由于大多数我们的命令,AddItemToCartCommand 被设计为 不可变对象。它的目的是在创建和验证后 保持命令的完整性,以便在处理程序中安全地处理它。因此,尽管 PropertyPathMapper 能够通过提供的获取器读取属性,但命令对象没有设置器。因此,PropertyPathMapper 无法将提交的表单数据映射到我们的对象。我们必须告诉表单组件如何进行操作(有关如何使用数据映射器实现此操作的完整解释和示例,请参阅 Bernhard Schussek (Webmozart) 的博客文章:"Value Objects in Symfony Forms")。

当然,您可以通过添加设置器或将命令属性设置为公共的来解决这个问题,但如上所述

您的类不应该因为基础设施约束而失去其本质。

我们已经看到 PropertyPathMapper 能够完美地读取我们的对象,并将它的属性映射到表单上。因此出现了新的 SimpleObjectMappersimple_object_mapper 选项

<?php

class AddItemToCartType extends AbstractType implements FormDataToObjectConverterInterface // <-- Implement this interface
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('reference', HiddenType::class)
            ->add('quantity', IntegerType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => AddItemToCartCommand::class,
            'simple_object_mapper' => $this, // <-- Set this option
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function convertFormDataToObject(array $data, $originalData)
    {
        // Tells the form how to build your object from its data:
        return new AddItemToCartCommand(
            $data['reference'],
            $data['quantity']
        );
    }
}

您的工作就是告诉表单如何根据提交的数据创建对象的实例,通过实现 FormDataToObjectConverterInterface::convertFormDataToObject(array $data, $originalData)

  • 此方法的第一 $data 参数是提交给表单的数据数组。
  • 第二个 $originalData 参数是在创建表单时提供给表单的原始数据,它可以被重新用于从表单本身不存在的数据中重新创建对象。

这段代码与我们在第一个控制器版本中编写的大致相同,但逻辑被移动到它应该属于的地方:表单类型内部。
作为奖励,对象由 Symfony 验证器组件正确验证。

高级用法

使用回调

除了实现 FormDataToObjectConverterInterface,您还可以简单地将可调用的函数作为 simple_object_mapper 选项的值传递

<?php

class AddItemToCartType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('reference', HiddenType::class)
            ->add('quantity', IntegerType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => AddItemToCartCommand::class,
            'simple_object_mapper' => function (array $data, $originalData) {
                  // Tells the form how to build your object from its data:
                  return new AddItemToCartCommand(
                      $data['reference'],
                      $data['quantity']
                  );
            }
        ]);
    }
}

处理转换错误

如果您无法将表单数据转换为对象,因为数据意外缺失或数据类型不正确,您应该抛出 TransformationFailedException
此异常被表单组件优雅地处理,通过捕获它并将其转换为表单错误。显示的错误消息是在 invalid_message 组件中设置的。

应使用适当的表单类型(例如:整数字段的 IntegerType)确保结构验证,并使用 Symfony 验证器组件的验证规则确保域验证。

将表单数据转换为null

当有必要的时,您需要在FormDataToObjectConverterInterface::convertFormDataToObject()方法内部添加自己的逻辑,以便根据提交的数据返回null而不是对象实例。

<?php

class MoneyType extends AbstractType implements FormDataToObjectConverterInterface 
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('amount', NumberType::class)
            ->add('currency')
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Money::class,
            'simple_object_mapper' => $this,
        ]);
    }

    /**
     * {@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 null;
        }

        return new Money($data['amount'], $data['currency']);
    }
}

# Money.php
class Money
{
    private $amount;
    private $currency;

    public function __construct($amount, $currency)
    {
        $this->amount = $amount;
        $this->currency = $currency;
    }

    public function getAmount() // ...
    public function getCurrency() // ...
}

将对象映射到表单

如果不可变对象有合适的getter,映射对象到表单通常不是您应该关心的事情。默认的PropertyPathhMapper实现将完美完成这项工作。

然而,对于大多数高级用法,也可以实现一个ObjectToFormDataConverterInterface接口,允许跳过原始映射器(大多数情况下是PropertyPathMapper)的实现,允许您通过将值对象转换为按字段名称索引的表单数据数组来自己映射数据到表单。

<?php

class MediaConverter implements FormDataToObjectConverterInterface, ObjectToFormDataConverterInterface 
{
    // ...

    /**
     * {@inheritdoc}
     *
     * @param Media|null $object
     */
    public function convertObjectToFormData($object)
    {
        if (null === $object) {
            return [];
        }

        $mediaTypeByClass = [
            Movie::class => 'movie',
            Book::class => 'book',
        ];

        if (!isset($mediaTypeByClass[get_class($object)])) {
            throw new TransformationFailedException('Unexpected object class');
        }

        // The returned array will be used to set data in each form fields identified by keys.
        return [
            'mediaType' => $mediaTypeByClass[get_class($object)],
            'author' => $object->getAuthor(),
        ];
    }
}

📝 请记住,TransformationFailedException的消息不会用于渲染表单错误。它将使用invalid_message选项值。然而,对于调试目的,设置它是很有用的。

✌️ 通过使用适当的ChoiceType字段,这个异常不应该发生,并且将显示关于意外字段值的适当消息。