php - 使用 Symfony2 的表单 - 具有一些不可变构造函数参数和 OneToMany 关联的 Doctrine Entity

标签 php symfony doctrine-orm symfony-forms

I have a OneToMany association between a Server entity and Client entities in the database. One server can have many clients. I want to make a form where the user can choose a server from a dropdown, fill in some details for a new client, and submit it.

目标

要创建一个表单,用户可以在其中将数据输入到Client 的字段中,请从下拉列表中选择一个Server,然后单击提交并获得此数据(以及association)通过 Doctrine 持续存在。

很简单吧?一定不行。我们会做到这一点。这是目前的漂亮形式:

Server Client Form

注意事项:

  • 服务器由 Server 实体 (EntityRepository::findAll()) 填充
  • 客户端是一个带有硬编码值的下拉列表
  • 端口、端点、用户名和密码都是文本字段

客户端实体

以我无限的智慧,我已经声明我的 Client 实体具有以下构造函数签名:

class Client
{
    /** -- SNIP -- **/
    public function __construct($type, $port, $endPoint, $authPassword, $authUsername);
    /** -- SNIP -- **/
}

这不会改变。要创建有效的 Client 对象,存在上述构造函数参数。它们不是可选的,如果没有在对象实例化时给出上述参数,则无法创建此对象。

潜在问题:

  • type 属性是不可变的。一旦创建了客户端,就无法更改类型。

  • 我没有type 的setter。它只是一个构造函数参数。这是因为一旦创建了客户端,您就不能更改类型。因此,我在实体级别强制执行此操作。因此,没有 setType()changeType() 方法。

  • 我没有标准的 setObject 命名约定。我声明要更改端口,例如,方法名称是 changePort() 而不是 setPort()。在使用 ORM 之前,这就是我要求我的对象 API 运行的方式。

服务器实体

我正在使用 __toString() 连接 nameipAddress 成员以显示在表单下拉列表中:

class Server 
{
    /** -- SNIP -- **/
    public function __toString()
    {
        return sprintf('%s - %s', $this->name, $this->ipAddress);
    }
    /** -- SNIP -- **/
}

自定义表单类型

我使用 Building Forms with Entities 作为我的代码的基线。

这是我为构建表单而创建的 ClientType:

class ClientType extends AbstractType
{
    /**
     * @var UrlGenerator
     */
    protected $urlGenerator;

    /**
     * @constructor
     *
     * @param UrlGenerator $urlGenerator
     */
    public function __construct(UrlGenerator $urlGenerator)
    {
        $this->urlGenerator = $urlGenerator;
    }

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        /** Dropdown box containing the server name **/
        $builder->add('server', 'entity', [
            'class' => 'App\Model\Entity\Server',
            'query_builder' => function(ServerRepository $serverRepository) {
                return $serverRepository->createQueryBuilder('s');
            },
            'empty_data' => '--- NO SERVERS ---'
        ]);

        /** Dropdown box containing the client names **/
        $builder->add('client', 'choice', [
            'choices' => [
                'transmission' => 'transmission',
                'deluge'       => 'deluge'
            ],
            'mapped' => false
        ]);

        /** The rest of the form elements **/
        $builder->add('port')
                ->add('authUsername')
                ->add('authPassword')
                ->add('endPoint')
                ->add('addClient', 'submit');

        $builder->setAction($this->urlGenerator->generate('admin_servers_add_client'))->setMethod('POST');
    }

    /**
     * {@inheritdoc}
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'App\Model\Entity\Client',
            'empty_data' => function(FormInterface $form) {
                return new Client(
                    $form->getData()['client'],
                    $form->getData()['port'],
                    $form->getData()['endPoint'],
                    $form->getData()['authPassword'],
                    $form->getData()['authUsername']
                );
            }
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'client';
    }
}

上面的代码实际上生成了要在客户端使用的表单(通过 twig)。

问题

首先也是最重要的,使用上面的代码,提交表单会给我:

NoSuchPropertyException in PropertyAccessor.php line 456: Neither the property "port" nor one of the methods "addPort()"/"removePort()", "setPort()", "port()", "__set()" or "__call()" exist and have public access in class "App\Model\Entity\Client".

所以它找不到端口方法。那是因为它是 changePort(),正如我之前解释的那样。我如何告诉它应该使用 changePort() 来代替?根据 docs,我必须为端口、端点等使用 entity 类型。但它们只是文本字段。我该如何以正确的方式解决这个问题?

我试过:

  • 在端口、authUsername 等上设置 ['mapped' => false]。这为所有客户端字段提供了 null,但 确实如此 似乎有相关的服务器详细信息。无论如何,$form->isValid() 都会返回 false。以下是 var_dump() 向我显示的内容:

Nope :(

  • 其他事情的组合,包括将每个字段设置为“实体”等等。

基本上,“它不起作用”。但这就是我所知道的。 我做错了什么?我一遍又一遍地阅读手册,但一切都相距甚远,以至于我不知道我是否应该使用 DataTransformerEntity Field Type 或其他方式。我几乎要完全放弃 Symfony/Forms,只用十分之一的时间自己写这个。

有人可以就如何到达我想去的地方给我一个可靠的答案吗?这也可能有助于 future 的用户:-)

最佳答案

There are a few problems with the above solution, so here's how I got it working!

空值

事实证明,在 setDefaultOptions() 中,代码:$form->getData['key'] 返回 null,因此屏幕截图中的所有 null .这需要更改为 $form->get('key')->getData()

return new Client(
    $form->get('client')->getData(),
    $form->get('port')->getData(),
    $form->get('endPoint')->getData(),
    $form->get('authPassword')->getData(),
    $form->get('authUsername')->getData()
);

结果,数据按预期通过,所有值都完好无损(id 除外)。

Twig Csrf

根据documentation您可以在表单选项中设置 csrf_protection => false。如果不这样做,则需要在表单中呈现隐藏的 csrf 字段:

{{ form_rest(form) }}

这会为您呈现其余的表单字段,包括隐藏的 _token 一个:

Symfony2 has a mechanism that helps to prevent cross-site scripting: they generate a CSRF token that have to be used for form validation. Here, in your example, you're not displaying (so not submitting) it with form_rest(form). Basically form_rest(form) will "render" every field that you didn't render before but that is contained into the form object that you've passed to your view. CSRF token is one of those values.

硅胶

这是我在解决上述问题后遇到的错误:

The CSRF token is invalid. Please try to resubmit the form.

我正在使用 Silex,在注册 FormServiceProvider 时,我有以下内容:

$app->register(new FormServiceProvider, [
    'form.secret' => uniqid(rand(), true)
]);

This Post显示 Silex 如何为您提供一些已弃用的 CsrfProvider 代码:

Turned out it was not due to my ajax, but because Silex gives you a deprecated DefaultCsrfProvider which uses the session ID itself as part of the token, and I change the ID randomly for security. Instead, explicitly telling it to use the new CsrfTokenManager fixes it, since that one generates a token and stores it in the session, such that the session ID can change without affecting the validity of the token.

因此,我不得不删除 form.secret 选项,并将以下内容添加到我的应用程序 Bootstrap 中,在注册表单提供程序之前:

/** Use a CSRF provider that does not depend on the session ID being constant. We change the session ID randomly */
$app['form.csrf_provider'] = $app->share(function ($app) {
    $storage = new Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage($app['session']);
    return new Symfony\Component\Security\Csrf\CsrfTokenManager(null, $storage);
});

通过上述修改,现在表单发布并且数据正确保存在数据库中,包括学说协会!

关于php - 使用 Symfony2 的表单 - 具有一些不可变构造函数参数和 OneToMany 关联的 Doctrine Entity,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24530776/

相关文章:

php - Doctrine 在多用途表上收集多行(单向无连接表 ManyToMany?)

php - Zend Framework 2 和 Doctrine 2 - 多个数据库的配置

php - 统计每个月的工作数量

php - 如何用一个序列化数据连接两列?

php - 使用外部 SQL 数据库使 Doctrine 实体保持最新

php - 使用 'OR condition' 的 Doctrine FindBy 方法?

php - 将所有用户事件存储为 session 值并在 session 结束时保存 session 值

php - 堆栈与排队?

php - 入门 Symfony 2 的最佳书籍、教程或博客是什么

php - Symfony2 表单集合 : How to remove entity from a collection?