数据转换适用于将一个字段数据格式转换成表单里显示的数据格式(并且可以重复提交)。在symfony内部已经有了很多这样的字段类型。举例,DateType类型在input文本框中被渲染成yyyy-MM-dd格式。在内部,一个数据转换器将开始的DateTime字段的值转换成yyyy-MM-dd字符串渲染到form,并在提交时返回DateTime对象。
注意:当一个表单字段设置了inherit_data配置时,数据转换器将不会应用到这一字段。
简单例子:清除用户输入的HTML
假设你有一个Task表单,有一个textarea类型的description字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// src/AppBundle/Form/TaskType.php namespace AppBundle\Form\Type; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Form\Extension\Core\Type\TextareaType; // ... class TaskType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('description', TextareaType::class); } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => 'AppBundle\Entity\Task', )); } // ... } |
但,这里有两个复杂的地方:
1.你的用户可能会输入一些html标签,你可能不都需要它:在表单提交之后,你需要调用strip_tags方法。
2.为了友好,在渲染表单时你可能想要把<br />标签转换成换行符\n,使得文本编辑起来更加人性化。
这是一个将自定义数据转换到description
字段的好机会。使用 CallbackTransformer
这个方法很容易去做到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// src/AppBundle/Form/TaskType.php namespace AppBundle\Form\Type; use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\Extension\Core\Type\TextareaType; // ... class TaskType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('description', TextareaType::class); $builder->get('description') ->addModelTransformer(new CallbackTransformer( // transform <br/> to \n so the textarea reads easier function ($originalDescription) { return preg_replace('#<br\s*/?>#i', "\n", $originalDescription); }, function ($submittedDescription) { // remove most HTML tags (but not br,p) $cleaned = strip_tags($submittedDescription, '<br><br/><p>'); // transform any \n to real <br/> return str_replace("\n", '<br/>', $cleaned); } )) ; } // ... } |
CallbackTransformer类用两个回调函数作为参数。第一个函数将原始的值转化为一个能在表单中渲染的格式。第二个函数做了相反的事情:他将提交后获取的值转化为你代码中需要的格式。
提示:这个addModelTransformer()方法接受任何实现DataTransformerInterface接口的对象- 这样你能够创建属于我们自己的类,而不是在表单中放入所有的逻辑(看下一章)。
当添加字段略微改变格式,你也可以添加转换器(transformer):
1 2 3 4 5 6 |
use Symfony\Component\Form\Extension\Core\Type\TextareaType; $builder->add( $builder->create('description', TextareaType::class) ->addModelTransformer(...) ); |
复杂的例子:将Issue编号转化成Isuse实体
比如说你有一个Task实体和一个Issue实体他们是 many-to-one(多对一)的映射关系(好像每一个任务都有一些关联的问题)。添加所有问题到一个listbox,他会变得很长,而且加载时间也变长了。你可以添加一个textbox让用户输入一些问题的编号来解决。
开始我们设置一个文本字段就和平时一样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// src/AppBundle/Form/TaskType.php namespace AppBundle\Form\Type; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; // ... class TaskType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('description', TextareaType::class) ->add('issue', TextType::class) ; } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => 'AppBundle\Entity\Task' )); } // ... } |
一个好的开始!如果你停止在这里,并提交表单,你的Task的issue属性就会是一个字符串(例如 55 )。你怎么把他变成一个实体提交呢?
创建转换器
你应该像之前一样使用CallbackTransformer。但是由于这个逻辑有些复杂,创建一个转换器将会使得TaskType表单类更加简单。
创建一个IssueToNumberTransformer类:他将会负责相互转化Issue编号和Issue实体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
// src/AppBundle/Form/DataTransformer/IssueToNumberTransformer.php namespace AppBundle\Form\DataTransformer; use AppBundle\Entity\Issue; use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; class IssueToNumberTransformer implements DataTransformerInterface { private $manager; public function __construct(ObjectManager $manager) { $this->manager = $manager; } /** * Transforms an object (issue) to a string (number). * * @param Issue|null $issue * @return string */ public function transform($issue) { if (null === $issue) { return ''; } return $issue->getId(); } /** * Transforms a string (number) to an object (issue). * * @param string $issueNumber * @return Issue|null * @throws TransformationFailedException if object (issue) is not found. */ public function reverseTransform($issueNumber) { // no issue number? It's optional, so that's ok if (!$issueNumber) { return; } $issue = $this->manager ->getRepository('AppBundle:Issue') // query for the issue with this id ->find($issueNumber) ; if (null === $issue) { // causes a validation error // this message is not shown to the user // see the invalid_message option throw new TransformationFailedException(sprintf( 'An issue with number "%s" does not exist!', $issueNumber )); } return $issue; } } |
就像第一个例子,转换器有两个方法。这个transform()负责将你代码中的数据转换为一个form表单渲染的数据格式(如:一个Issue对象转换为一个id字符串)。这个reverseTransform()方法正好相反:他将提交的数据转换成你代码想要的数据(如:把一个id转换为Issue对象)。
如果验证发生错误,你可以抛出TransformationFailedException。但是这个异常信息就不要给你的用户看了。你使用invalid_message来设置消息(详见下面)。
当null被传递到transform()方法时,你的转换器应该返回一个和它类型相等的值(例如:一个空字符串,整型的0,或者是浮点数0.0)。
使用这个转换器
下一步,你将在TaskType中实例化你的IssueToNumberTransformer类并添加他到issue字段。要做到这一点,你将需要一个实体管理(entity manager)(因为IssueToNumberTransformer需要他)。
没有问题!仅仅给TaskType添加一个__construct()函数并把它注册为一个服务传入entity管理即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// src/AppBundle/Form/TaskType.php namespace AppBundle\Form\Type; use AppBundle\Form\DataTransformer\IssueToNumberTransformer; use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; // ... class TaskType extends AbstractType { private $manager; public function __construct(ObjectManager $manager) { $this->manager = $manager; } public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('description', TextareaType::class) ->add('issue', TextType::class, array( // validation message if the data transformer fails 'invalid_message' => 'That is not a valid issue number', )); // ... $builder->get('issue') ->addModelTransformer(new IssueToNumberTransformer($this->manager)); } // ... } |
在你的配置文件中定义一个表单类型作为一个服务:
1 2 3 4 5 6 7 |
# src/AppBundle/Resources/config/services.yml services: app.form.type.task: class: AppBundle\Form\Type\TaskType arguments: ["@doctrine.orm.entity_manager"] tags: - { name: form.type } |
提示:更多表单类型注册为服务的信息,请阅读 register your form type as a service.
现在,你能够很容易的使用你的TaskType:
1 2 3 4 |
// e.g. in a controller somewhere $form = $this->createForm(TaskType::class, $task); // ... |
酷,你完成了!你的用户将能够在text字段输入一个issue编号来把他转换成一个Issue对象。这意味着,在成功的提交之后,表单组件将会向 Task::setIssue() 传递一个真正的 Issue 对象而不是问题数字。
如果issue没有被找到的话,一个表单字段错误将会产生,并且invalid_message这个字段能够控制错误信息。
注意:当你添加一个转换器时你要小心。举例,下面代码是错误的,由于转换器将会被用于整个表单而不是仅仅这个字段:
1 2 3 4 |
// THIS IS WRONG - TRANSFORMER WILL BE APPLIED TO THE ENTIRE FORM // see above example for correct code $builder->add('issue', TextType::class) ->addModelTransformer($transformer); |
创建一个可以重复使用的 issue_selector 字段:
在上面的例子中,你转换了一个普通的text字段。但如果你要做很多这样的转换,最好是创建一个自定义的表单类型,他就可以自动完成。
首先,创建一个自定义的字段类型类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// src/AppBundle/Form/IssueSelectorType.php namespace AppBundle\Form; use AppBundle\Form\DataTransformer\IssueToNumberTransformer; use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class IssueSelectorType extends AbstractType { private $manager; public function __construct(ObjectManager $manager) { $this->manager = $manager; } public function buildForm(FormBuilderInterface $builder, array $options) { $transformer = new IssueToNumberTransformer($this->manager); $builder->addModelTransformer($transformer); } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'invalid_message' => 'The selected issue does not exist', )); } public function getParent() { return TextType::class; } } |
好!他将像一个text字段一样渲染(getParent()做了指定),但他自动有一个数据转换器并默认配置invalid_message。
接下来,将你的类型注册为一个服务并标注form.type标签,这样他就被认定为是一个自定义的字段类型了:
1 2 3 4 5 6 7 |
# app/config/services.yml services: app.type.issue_selector: class: AppBundle\Form\IssueSelectorType arguments: ['@doctrine.orm.entity_manager'] tags: - { name: form.type } |
现在,无论什么使用你需要使用你的特殊issue_selector字段类型,他都非常的容易:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// src/AppBundle/Form/TaskType.php namespace AppBundle\Form\Type; use AppBundle\Form\DataTransformer\IssueToNumberTransformer; use Symfony\Component\Form\Extension\Core\Type\TextareaType; // ... class TaskType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('description', TextareaType::class) ->add('issue', IssueSelectorType::class) ; } // ... } |
关于Model(模型)和View Transformers(视图转换器)
上面的例子的转换器是一个“Model”转换器。实时上,共有两种类型的转换器,又有三种不同类型的基础数据。
在任何表单中,都有三种不同类型的数据:
1. Model data (模型数据)- 这个数据在你的应用程序内部使用(例如一个Issue对象)。如果你调用Form::getData()或者Form::setData(),你就可以处理模型数据。
2.Norm Data (普通数据) – 这是一个你的普通版本数据,并且这个数据和你的modle数据一样常见(尽管我们的例子中没有)。她通常不会被直接应用。
3.View Data (视图数据) – 这是表单字段自动填充的数据格式。用户也很有可能提交这种格式的数据。当你调用 Form::submit($data)时,$data 就是“视图”格式的数据。
这两种不同类型的转换器可以帮助我们相互转换这些类型数据:
Model transformers:
transform
: “model data” => “norm data”reverseTransform
: “norm data” => “model data”
View transformers:
transform
: “norm data” => “view data”reverseTransform
: “view data” => “norm data”
你需要使用那种转换器取决于你的实际情况。
如果你想使用视图转换器(view transformer)就调用addViewTransformer。
为什么在这里要使用模型转换器?
在这个例子中,字段类型是一个text,同时一个text字段总是比较简单,这个格式在“norm”和“view”中。因为在这里model转换器做适合转换(转换表单格式—-字符串issue编号—-模型格式—-Issue对象)。
转换器的区别是微妙的,你应该考虑‘norm’数据字段是什么样子。举例来说,text字段的普通数据就是一个字符串,但是一个date字段就是一个DataTime对象。
提示:一个普遍的规律,规范化的数据应当包含尽可能多的信息。