symfony:我们不能有一个隐藏的实体字段吗?

Posted

技术标签:

【中文标题】symfony:我们不能有一个隐藏的实体字段吗?【英文标题】:symfony : can't we have a hidden entity field? 【发布时间】:2015-01-21 13:53:23 【问题描述】:

我在 symfony 中渲染一个带有实体字段的表单。

当我选择常规实体字段时效果很好。

$builder
    ->add('parent','entity',array(
            'class' => 'AppBundle:FoodAnalytics\Recipe',
            'attr' => array(
                'class' => 'hidden'
            )
        ))

当我选择 ->add('parent','hidden') 时会抛出以下错误:

表单的视图数据应为标量、数组或 \ArrayAccess 的实例,但它是类的实例 AppBundle\Entity\FoodAnalytics\Recipe。您可以通过以下方式避免此错误 将“data_class”选项设置为 “AppBundle\Entity\FoodAnalytics\Recipe”或通过添加视图 转换类实例的转换器 AppBundle\Entity\FoodAnalytics\Recipe 到标量、数组或实例 \ArrayAccess 的。 500 内部服务器错误 - LogicException

我们不能有隐藏的实体字段吗?为什么不?我是否必须放置另一个隐藏字段来检索实体 ID?

编辑:

基本上,我要做的是在显示表单之前对表单进行水合,但防止用户更改其字段之一(此处为父项)。 这是因为我需要将 Id 作为参数传递,而我无法在表单操作 url 中进行。

【问题讨论】:

【参考方案1】:

我认为您只是对字段类型及其各自代表的内容感到困惑。

entity 字段是choice 字段的类型。选择字段旨在包含用户在表单中可选择的值。当这个表单被渲染时,Symfony 会根据实体字段的底层类生成一个可能的选择列表,列表中每个选择的值是各自实体的 id。提交表单后,Symfony 将为您生成一个代表所选实体的对象。 entity 字段通常用于呈现实体关联(例如,您可以选择分配给userroles 列表)。

如果您只是尝试为实体的 ID 字段创建占位符,那么您将使用 hidden 输入。但这仅在您创建的表单类表示一个实体时才有效(即表单的data_class 指的是您定义的实体)。然后,ID 字段将正确映射到由表单的 data_class 定义的类型的实体的 ID。

编辑:下面描述的针对您的特定情况的一种解决方案是创建一个新的字段类型(我们称之为 EntityHidden),它扩展了 hidden 字段类型,但处理数据转换以与实体/id 相互转换。这样,您的表单将包含实体 ID 作为隐藏字段,但一旦提交表单,应用程序将有权访问实体本身。当然,转换是由数据转换器执行的。

以下是此类实现的示例,供后人参考:

namespace My\Bundle\Form\Extension\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\DataTransformerInterface;

/**
 * Entity hidden custom type class definition
 */
class EntityHiddenType extends AbstractType

    /**
     * @var DataTransformerInterface $transformer
     */
     private $transformer;

    /**
     * Constructor
     *
     * @param DataTransformerInterface $transformer
     */
    public function __construct(DataTransformerInterface $transformer)
    
        $this->transformer = $transformer;
    

    /**
     * @inheritDoc
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    
        // attach the specified model transformer for this entity list field
        // this will convert data between object and string formats
        $builder->addModelTransformer($this->transformer);
    

    /**
     * @inheritDoc
     */
    public function getParent()
    
        return 'hidden';
    

    /**
     * @inheritDoc
     */
    public function getName()
    
        return 'entityhidden';
    

请注意,在表单类型类中,您只需将隐藏实体分配给其相应的表单字段属性(在表单模型/数据类中),Symfony 将使用 ID 正确生成隐藏的输入 html实体作为它的值。希望对您有所帮助。

【讨论】:

嗯,我有一个带有 ID 的实际实体,并且该实体有同一个类的父级。这种父子关系是使用 Doctrine 树扩展映射的。我正在构建一个允许从现有实体创建新实体的过程,并且在这样做时,我将父字段设置为旧实体。所以我的父字段是映射的(一对多关系),但我不希望用户能够修改它......这里我无法更改整个类型的 data_class,因为它已经设置了 看看数据转换器:symfony.com/doc/current/cookbook/form/data_transformers.html它们将允许您将实体转换为ID。请注意,使用隐藏字段不会阻止用户更改值。只是让它更具挑战性。 看看Gregwar/FormBundle,它提供了“entity_id”类型(阅读“实体标识符”)。 我认为这是 Symfony 缺少的功能,他们应该添加一个选项来呈现隐藏的输入(就像他们渲染选择或单选或复选框一样)组件。【参考方案2】:

刚刚在 Symfony 3 上制作了这个,发现它与这里已经发布的有点不同,所以我认为它值得分享。

我刚刚制作了一个通用数据转换器,可以轻松地在所有表单类型中重复使用。你只需要传入你的表单类型就可以了。无需创建自定义表单类型。

首先让我们看一下数据转换器:

<?php

namespace AppBundle\Form;

use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;

/**
 * Class EntityHiddenTransformer
 *
 * @package AppBundle\Form
 * @author  Francesco Casula <fra.casula@gmail.com>
 */
class EntityHiddenTransformer implements DataTransformerInterface

    /**
     * @var ObjectManager
     */
    private $objectManager;

    /**
     * @var string
     */
    private $className;

    /**
     * @var string
     */
    private $primaryKey;

    /**
     * EntityHiddenType constructor.
     *
     * @param ObjectManager $objectManager
     * @param string        $className
     * @param string        $primaryKey
     */
    public function __construct(ObjectManager $objectManager, $className, $primaryKey)
    
        $this->objectManager = $objectManager;
        $this->className = $className;
        $this->primaryKey = $primaryKey;
    

    /**
     * @return ObjectManager
     */
    public function getObjectManager()
    
        return $this->objectManager;
    

    /**
     * @return string
     */
    public function getClassName()
    
        return $this->className;
    

    /**
     * @return string
     */
    public function getPrimaryKey()
    
        return $this->primaryKey;
    

    /**
     * Transforms an object (entity) to a string (number).
     *
     * @param  object|null $entity
     *
     * @return string
     */
    public function transform($entity)
    
        if (null === $entity) 
            return '';
        

        $method = 'get' . ucfirst($this->getPrimaryKey());

        // Probably worth throwing an exception if the method doesn't exist
        // Note: you can always use reflection to get the PK even though there's no public getter for it

        return $entity->$method();
    

    /**
     * Transforms a string (number) to an object (entity).
     *
     * @param  string $identifier
     *
     * @return object|null
     * @throws TransformationFailedException if object (entity) is not found.
     */
    public function reverseTransform($identifier)
    
        if (!$identifier) 
            return null;
        

        $entity = $this->getObjectManager()
            ->getRepository($this->getClassName())
            ->find($identifier);

        if (null === $entity) 
            // causes a validation error
            // this message is not shown to the user
            // see the invalid_message option
            throw new TransformationFailedException(sprintf(
                'An entity with ID "%s" does not exist!',
                $identifier
            ));
        

        return $entity;
    

所以想法是通过传递对象管理器、要使用的实体以及字段名称来调用它,以获取实体 ID。

基本上是这样的:

new EntityHiddenTransformer(
    $this->getObjectManager(),
    Article::class, // in your case this would be FoodAnalytics\Recipe::class
    'articleId' // I guess this for you would be recipeId?
)

现在让我们把它们放在一起。我们只需要表单类型和一些 YAML 配置就可以了。

<?php

namespace AppBundle\Form;

use AppBundle\Entity\Article;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Class JustAFormType
 *
 * @package AppBundle\CmsBundle\Form
 * @author  Francesco Casula <fra.casula@gmail.com>
 */
class JustAFormType extends AbstractType

    /**
     * @var ObjectManager
     */
    private $objectManager;

    /**
     * JustAFormType constructor.
     *
     * @param ObjectManager $objectManager
     */
    public function __construct(ObjectManager $objectManager)
    
        $this->objectManager = $objectManager;
    

    /**
     * @return ObjectManager
     */
    public function getObjectManager()
    
        return $this->objectManager;
    

    /**
     * @inheritdoc
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    
        $builder
            ->add('article', HiddenType::class)
            ->add('save', SubmitType::class);

        $builder
            ->get('article')
            ->addModelTransformer(new EntityHiddenTransformer(
                $this->getObjectManager(),
                Article::class,
                'articleId'
            ));
    

    /**
     * @inheritdoc
     */
    public function configureOptions(OptionsResolver $resolver)
    
        $resolver->setDefaults([
            'data_class' => 'AppBundle\Entity\MyEntity',
        ]);
    

然后在您的services.yml 文件中:

app.form.type.article:
    class: AppBundle\Form\JustAFormType
    arguments: ["@doctrine.orm.entity_manager"]
    tags:
        -  name: form.type 

在你的控制器中:

$form = $this->createForm(JustAFormType::class, new MyEntity());
$form->handleRequest($request);

就是这样:-)

【讨论】:

【参考方案3】:

这可以通过表单主题相当干净地实现,使用标准的hidden 字段主题代替实体主题。我认为使用转换器可能是矫枉过正,因为隐藏和选择字段将提供相同的格式。

% block _recipe_parent_widget %
    %- set type = 'hidden' -%
     block('form_widget_simple') 
% endblock %

【讨论】:

但是使用这种方法,您必须自己(在您的控制器中)获取提交的id 的实体,检查具有此id 的实体是否确实存在,如果找不到实体。恕我直言,这绝对是错误的方法! 没有。它与实体类型的工作方式相同。您无需更改表单类型 - 只需为小部件设置主题。唯一的区别是它将呈现&lt;input type="hidden"&gt; 而不是&lt;select&gt;【参考方案4】:

在 Symfony 5 中,我使用实现 DataTransformerInterface 接口的 Hidden 类型的解决方案。

<?php

namespace App\Form\Type;

use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;

/**
 * Defines the custom form field type used to add a hidden entity
 *
 * See https://symfony.com/doc/current/form/create_custom_field_type.html
 */
class EntityHiddenType extends HiddenType implements DataTransformerInterface


    /** @var ManagerRegistry $dm */
    private $dm;

    /** @var string $entityClass */
    private $entityClass;

    /**
     *
     * @param ManagerRegistry $doctrine
     */
    public function __construct(ManagerRegistry $doctrine)
    
        $this->dm = $doctrine;
    

    /**
     *
     * @inheritdoc
     */
    public function buildForm(FormBuilderInterface $builder, array $options): void
    
        // Set class, eg: App\Entity\RuleSet
        $this->entityClass = sprintf('App\Entity\%s', ucfirst($builder->getName()));
        $builder->addModelTransformer($this);
    

    public function transform($data): string
    
        // Modified from comments to use instanceof so that base classes or interfaces can be specified
        if (null === $data || !$data instanceof $this->entityClass) 
            return '';
        

        $res = $data->getId();

        return $res;
    

    public function reverseTransform($data)
    
        if (!$data) 
            return null;
        

        $res = null;
        try 
            $rep = $this->dm->getRepository($this->entityClass);
            $res = $rep->findOneBy(array(
                "id" => $data
            ));
        
        catch (\Exception $e) 
            throw new TransformationFailedException($e->getMessage());
        

        if ($res === null) 
            throw new TransformationFailedException(sprintf('A %s with id "%s" does not exist!', $this->entityClass, $data));
        

        return $res;
    

并使用表单中的字段:

use App\Form\Type\EntityHiddenType;

public function buildForm(FormBuilderInterface $builder, array $options): void

    // Field name must match entity class, eg 'ruleSet' for App\Entity\RuleSet
    $builder->add('ruleSet', EntityHiddenType::class);

【讨论】:

虽然此代码可能会解决问题,including an explanation 关于如何以及为什么解决问题将真正有助于提高您的帖子质量,并可能导致更多的赞成票。请记住,您正在为将来的读者回答问题,而不仅仅是现在提问的人。请edit您的回答添加解释并说明适用的限制和假设。【参考方案5】:

无需创建新的转换器和类型类的快速解决方案。当您想从数据库中预填充相关实体时。

// Hidden selected single group
$builder->add('idGroup', 'entity', array(
    'label' => false,
    'class' => 'MyVendorCoreBundle:Group',
    'query_builder' => function (EntityRepository $er) 
        $qb = $er->createQueryBuilder('c');
        return $qb->where($qb->expr()->eq('c.groupid', $this->groupId()));
    ,
    'attr' => array(
        'class' => 'hidden'
    )
));

这会产生一个隐藏的选择,例如:

<select id="mytool_idGroup" name="mytool[idGroup]" class="hidden">
    <option value="1">MyGroup</option>
</select>

但是,是的,我同意通过使用 DataTransformer 付出更多的努力,您可以实现以下目标:

<input type="hidden" value="1" id="mytool_idGroup" name="mytool[idGroup]"/>

【讨论】:

【参考方案6】:

这将满足您的需求:

$builder->add('parent', 'hidden', array('property_path' => 'parent.id'));

【讨论】:

以上是关于symfony:我们不能有一个隐藏的实体字段吗?的主要内容,如果未能解决你的问题,请参考以下文章

Symfony ApiPlatform:在实体中作为序列化器/组的角色是个好主意吗?

将带有教义查询的虚拟字段添加到 symfony 实体

在 Symfony 中,如何翻译实体中的关联字段(外键)?

Symfony2 表单 > 实体字段类型 > 查询构建器 > 可能的子选择?

Symfony - 通过不包含密码字段的表单对部分用户实体更新的尴尬密码长度违规

现有数据库中的 Symfony Doctrine Sluggable 扩展