使用symfony 2,我愿意创建一个新的字段类型,它结合了实体字段类型的行为和集合字段类型之一: - 如果用户选择现有实体,则新实体的集合为空 - 如果用户创建新实体,则不需要第一个字段
你知道如何继续吗?我可以重用现有的symfony类型吗?我在哪里放置逻辑(如果旧的,不需要集合,如果不需要新的实体)?
Thansk很多
答案 0 :(得分:6)
我终于明白了!哇,这并不容易。
所以基本上,当表单类型是实体类型时,使用javascript向select中添加一个新条目会触发Symfony \ Component \ Form \ Exception \ TransformationFailedException。
此异常来自ChoicesToValuesTransformer的reverseTransform方法中的ChoiceListInterface上调用的getChoicesForValues方法。这个DataTransformer在ChoiceType中使用,所以为了克服这个问题,我不得不构建一个扩展ChoiceType的新类型,只替换它的一小部分。
使其运作的步骤:
创建一个新类型:
<?php
namespace AppBundle\Form\Type;
use AppBundle\Form\DataTransformer\ChoicesToValuesTransformer;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManager;
use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener;
use Symfony\Component\Form\Extension\Core\EventListener\FixCheckboxInputListener;
use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToBooleanArrayTransformer;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class TagType extends ChoiceType
{
/**
* @var ManagerRegistry
*/
protected $registry;
/**
* @var array
*/
private $choiceListCache = array();
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* @var EntityManager
*/
private $entityManager;
public function __construct(EntityManager $entityManager, ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null)
{
$this->registry = $registry;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->entityManager = $entityManager;
$this->propertyAccessor = $propertyAccessor;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!$options['choice_list'] && !is_array($options['choices']) && !$options['choices'] instanceof \Traversable) {
throw new LogicException('Either the option "choices" or "choice_list" must be set.');
}
if ($options['expanded']) {
// Initialize all choices before doing the index check below.
// This helps in cases where index checks are optimized for non
// initialized choice lists. For example, when using an SQL driver,
// the index check would read in one SQL query and the initialization
// requires another SQL query. When the initialization is done first,
// one SQL query is sufficient.
$preferredViews = $options['choice_list']->getPreferredViews();
$remainingViews = $options['choice_list']->getRemainingViews();
// Check if the choices already contain the empty value
// Only add the empty value option if this is not the case
if (null !== $options['placeholder'] && 0 === count($options['choice_list']->getChoicesForValues(array('')))) {
$placeholderView = new ChoiceView(null, '', $options['placeholder']);
// "placeholder" is a reserved index
$this->addSubForms($builder, array('placeholder' => $placeholderView), $options);
}
$this->addSubForms($builder, $preferredViews, $options);
$this->addSubForms($builder, $remainingViews, $options);
if ($options['multiple']) {
$builder->addViewTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list']));
$builder->addEventSubscriber(new FixCheckboxInputListener($options['choice_list']), 10);
} else {
$builder->addViewTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'], $builder->has('placeholder')));
$builder->addEventSubscriber(new FixRadioInputListener($options['choice_list'], $builder->has('placeholder')), 10);
}
} else {
if ($options['multiple']) {
$builder->addViewTransformer(new ChoicesToValuesTransformer($options['choice_list']));
} else {
$builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list']));
}
}
if ($options['multiple'] && $options['by_reference']) {
// Make sure the collection created during the client->norm
// transformation is merged back into the original collection
$builder->addEventSubscriber(new MergeCollectionListener(true, true));
}
if ($options['multiple']) {
$builder
->addEventSubscriber(new MergeDoctrineCollectionListener())
->addViewTransformer(new CollectionToArrayTransformer(), true)
;
}
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$choiceListCache = & $this->choiceListCache;
$choiceList = function (Options $options) use (&$choiceListCache) {
// Harden against NULL values (like in EntityType and ModelType)
$choices = null !== $options['choices'] ? $options['choices'] : array();
// Reuse existing choice lists in order to increase performance
$hash = hash('sha256', serialize(array($choices, $options['preferred_choices'])));
if (!isset($choiceListCache[$hash])) {
$choiceListCache[$hash] = new SimpleChoiceList($choices, $options['preferred_choices']);
}
return $choiceListCache[$hash];
};
$emptyData = function (Options $options) {
if ($options['multiple'] || $options['expanded']) {
return array();
}
return '';
};
$emptyValue = function (Options $options) {
return $options['required'] ? null : '';
};
// for BC with the "empty_value" option
$placeholder = function (Options $options) {
return $options['empty_value'];
};
$placeholderNormalizer = function (Options $options, $placeholder) {
if ($options['multiple']) {
// never use an empty value for this case
return;
} elseif (false === $placeholder) {
// an empty value should be added but the user decided otherwise
return;
} elseif ($options['expanded'] && '' === $placeholder) {
// never use an empty label for radio buttons
return 'None';
}
// empty value has been set explicitly
return $placeholder;
};
$compound = function (Options $options) {
return $options['expanded'];
};
$resolver->setDefaults(array(
'multiple' => false,
'expanded' => false,
'choice_list' => $choiceList,
'choices' => array(),
'preferred_choices' => array(),
'empty_data' => $emptyData,
'empty_value' => $emptyValue, // deprecated
'placeholder' => $placeholder,
'error_bubbling' => false,
'compound' => $compound,
// The view data is always a string, even if the "data" option
// is manually set to an object.
// See https://github.com/symfony/symfony/pull/5582
'data_class' => null,
));
$resolver->setNormalizers(array(
'empty_value' => $placeholderNormalizer,
'placeholder' => $placeholderNormalizer,
));
$resolver->setAllowedTypes(array(
'choice_list' => array('null', 'Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface'),
));
$choiceListCache = & $this->choiceListCache;
$registry = $this->registry;
$propertyAccessor = $this->propertyAccessor;
$type = $this;
$loader = function (Options $options) use ($type) {
if (null !== $options['query_builder']) {
return $type->getLoader($options['em'], $options['query_builder'], $options['class']);
}
};
$choiceList = function (Options $options) use (&$choiceListCache, $propertyAccessor) {
// Support for closures
$propertyHash = is_object($options['property'])
? spl_object_hash($options['property'])
: $options['property'];
$choiceHashes = $options['choices'];
// Support for recursive arrays
if (is_array($choiceHashes)) {
// A second parameter ($key) is passed, so we cannot use
// spl_object_hash() directly (which strictly requires
// one parameter)
array_walk_recursive($choiceHashes, function (&$value) {
$value = spl_object_hash($value);
});
} elseif ($choiceHashes instanceof \Traversable) {
$hashes = array();
foreach ($choiceHashes as $value) {
$hashes[] = spl_object_hash($value);
}
$choiceHashes = $hashes;
}
$preferredChoiceHashes = $options['preferred_choices'];
if (is_array($preferredChoiceHashes)) {
array_walk_recursive($preferredChoiceHashes, function (&$value) {
$value = spl_object_hash($value);
});
}
// Support for custom loaders (with query builders)
$loaderHash = is_object($options['loader'])
? spl_object_hash($options['loader'])
: $options['loader'];
// Support for closures
$groupByHash = is_object($options['group_by'])
? spl_object_hash($options['group_by'])
: $options['group_by'];
$hash = hash('sha256', json_encode(array(
spl_object_hash($options['em']),
$options['class'],
$propertyHash,
$loaderHash,
$choiceHashes,
$preferredChoiceHashes,
$groupByHash,
)));
if (!isset($choiceListCache[$hash])) {
$choiceListCache[$hash] = new EntityChoiceList(
$options['em'],
$options['class'],
$options['property'],
$options['loader'],
$options['choices'],
$options['preferred_choices'],
$options['group_by'],
$propertyAccessor
);
}
return $choiceListCache[$hash];
};
$emNormalizer = function (Options $options, $em) use ($registry) {
/* @var ManagerRegistry $registry */
if (null !== $em) {
if ($em instanceof ObjectManager) {
return $em;
}
return $registry->getManager($em);
}
$em = $registry->getManagerForClass($options['class']);
if (null === $em) {
throw new RuntimeException(sprintf(
'Class "%s" seems not to be a managed Doctrine entity. '.
'Did you forget to map it?',
$options['class']
));
}
return $em;
};
$resolver->setDefaults(array(
'em' => null,
'property' => null,
'query_builder' => null,
'loader' => $loader,
'choices' => null,
'choice_list' => $choiceList,
'group_by' => null,
));
$resolver->setRequired(array('class'));
$resolver->setNormalizers(array(
'em' => $emNormalizer,
));
$resolver->setAllowedTypes(array(
'em' => array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager'),
'loader' => array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface'),
));
}
/**
* @return string
*/
public function getName()
{
return 'fmu_tag';
}
/**
* Return the default loader object.
*
* @param ObjectManager $manager
* @param mixed $queryBuilder
* @param string $class
*
* @return ORMQueryBuilderLoader
*/
public function getLoader(ObjectManager $manager, $queryBuilder, $class)
{
return new ORMQueryBuilderLoader(
$queryBuilder,
$manager,
$class
);
}
/**
* Adds the sub fields for an expanded choice field.
*
* @param FormBuilderInterface $builder The form builder.
* @param array $choiceViews The choice view objects.
* @param array $options The build options.
*/
private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options)
{
foreach ($choiceViews as $i => $choiceView) {
if (is_array($choiceView)) {
// Flatten groups
$this->addSubForms($builder, $choiceView, $options);
} else {
$choiceOpts = array(
'value' => $choiceView->value,
'label' => $choiceView->label,
'translation_domain' => $options['translation_domain'],
'block_name' => 'entry',
);
if ($options['multiple']) {
$choiceType = 'checkbox';
// The user can check 0 or more checkboxes. If required
// is true, he is required to check all of them.
$choiceOpts['required'] = false;
} else {
$choiceType = 'radio';
}
$builder->add($i, $choiceType, $choiceOpts);
}
}
}
}
在服务中注册类型:
tag.type:
class: %tag.type.class%
arguments: [@doctrine.orm.entity_manager, @doctrine ,@property_accessor]
tags:
- { name: form.type, alias: fmu_tag }
为复制选项的类型创建一个新视图:
{#app/Resources/views/Form/fmu_tag.html.twig#}
{% block fmu_tag_widget %}
{% if expanded %}
{{- block('choice_widget_expanded') -}}
{% else %}
{{- block('choice_widget_collapsed') -}}
{% endif %}
{% endblock %}
在twig config.yml中注册视图:
# Twig Configuration
twig:
form:
resources:
- 'Form/fmu_tag.html.twig'
创建一个新的ChoiceToValueDataTransformer,替换choiceType
中使用的默认类<?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 AppBundle\Form\DataTransformer;
use AppBundle\Entity\Core\Tag;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoicesToValuesTransformer implements DataTransformerInterface
{
private $choiceList;
/**
* Constructor.
*
* @param ChoiceListInterface $choiceList
*/
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
/**
* @param array $array
*
* @return array
*
* @throws TransformationFailedException If the given value is not an array.
*/
public function transform($array)
{
if (null === $array) {
return array();
}
if (!is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
return $this->choiceList->getValuesForChoices($array);
}
/**
* @param array $array
*
* @return array
*
* @throws TransformationFailedException If the given value is not an array
* or if no matching choice could be
* found for some given value.
*/
public function reverseTransform($array)
{
if (null === $array) {
return array();
}
if (!is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
$choices = $this->choiceList->getChoicesForValues($array);
if (count($choices) !== count($array)) {
$missingChoices = array_diff($array, $this->choiceList->getValues());
$choices = array_merge($choices, $this->transformMissingChoicesToEntities($missingChoices));
}
return $choices;
}
public function transformMissingChoicesToEntities(Array $missingChoices)
{
$newChoices = array_map(function($choice){
return new Tag($choice);
}, $missingChoices);
return $newChoices;
}
}
在此文件的最后一个方法中抓取:transformMissingChoicesToEntities 这是我失踪的地方,我创建了一个新的实体。因此,如果您想要使用所有这些,您需要调整新标签($ choice)即。用你自己的新实体替换它。
因此,您添加集合的表单现在使用您的新类型:
$builder
->add('tags', 'fmu_tag', array(
'by_reference' => false,
'required' => false,
'class' => 'AppBundle\Entity\Core\Tag',
'multiple' => true,
'label'=>'Tags',
));
为了创建新的选择,我正在使用select2控件。 在javascripts中添加文件:http://select2.github.io 在您的视图中添加以下代码:
<script>
$(function() {
$('#appbundle_marketplace_product_ingredient_tags').select2({
closeOnSelect: false,
multiple: true,
placeholder: 'Tapez quelques lettres',
tags: true,
tokenSeparators: [',', ' ']
});
});
</script>
就是这样,您最好选择现有实体或从select2生成的新条目中创建新实体。
答案 1 :(得分:1)
对于这种行为,您不应该真正需要一种全新的表单类型(尽管如果您愿意,您当然可以创建一种)。
查看Symfony dynamic form modification,其中包含根据实体是否为“新”修改表单字段的示例。您可以从此开始,并根据您的需要进行修改。
如果您在从Controller中创建表单时已经知道了什么,那么您可以通过标记要显示的内容的选项。例如,从您的控制器:
$form = $this->createForm(
new MyType(),
$entity,
array('show_my_entity_collection' => false)
);
然后在表单中输入:
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['show_my_entity_collection'])
{
$builder->add('entity', 'entity', array(
'class' => 'MyBundle:MyEntity',
'required' => false,
'query_builder' => function(MyEntityRepository $repository) {
return $repository->findAll();
},
));
}
// rest of form builder here
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyBundle\Entity\MyEntity',
'show_my_entity_collection' => true,
));
}
答案 2 :(得分:0)
如果您需要创建新的字段类型,使用新模板,您可以在此处查看如何执行此操作: http://symfony.com/doc/current/cookbook/form/create_custom_field_type.html