具有Symfony2的表单 - 具有一些不可变构造函数参数和OneToMany关联的Doctrine实体

时间:2014-07-02 12:10:52

标签: php symfony doctrine-orm symfony-forms

  

我在数据库中的OneToMany实体和Server实体之间存在Client关联。 一台服务器可以拥有多个客户端。我想创建一个表单,用户可以从下拉列表中选择服务器,填写新客户端的一些详细信息,然后提交。

目标

要创建用户可以在Client的字段中输入数据的表单,请从下拉列表中选择Server,然后单击“提交”并通过Doctrine保留此数据(和关联)。

简单,对吧?一定不行。我们会做到这一点。这是现在的漂亮形式:

Server Client Form

注意事项:

  • 服务器从Server实体(EntityRepository::findAll()
  • 填充
  • 客户端是包含硬编码值的下拉列表
  • 端口,端点,用户名和密码均为文本字段

客户实体

在我无限的智慧中,我宣称我的Client实体具有以下构造函数签名:

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

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

潜在问题

  • type属性是不可变的。创建客户端后,您无法更改类型。

  • 我没有type的设定器。它只是构造函数参数。这是因为一旦创建了客户端,您就无法更改类型。因此,我在实体层面强制执行此操作。因此,没有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)。

问题

首先,使用上面的代码,提交表单会给我:

   PropertyAccessor.php 第456行中的

NoSuchPropertyException :   属性“port”和方法“addPort()”/“removePort()”,“setPort()”,“port()”,“__ set()”或“__call()”都不存在并且公开在“App \ Model \ Entity \ Client”类中访问。

因此无法找到port方法。那是因为我之前解释的是changePort()。我如何告诉它应该使用changePort()代替?根据{{​​3}},我必须使用entity类型的端口,endPoint等。但它们只是文本字段。我该如何以正确的方式解决这个问题?

我试过了:

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

Nope :(

  • 涉及将每个字段设置为“实体”的其他内容的组合,以及更多..

基本上,“它不起作用”。但这就是我所拥有的。 我做错了什么?我一遍又一遍地阅读手册,但一切都相差甚远,以至于我不知道我是否应该使用docsDataTransformer或其他方式。我已经接近完全废弃Symfony / Forms,只是在十分之一的时间里自己写这个。

有人可以给我一个关于如何到达我想要的地方的可靠答案吗?这也可以帮助未来的用户: - )

1 个答案:

答案 0 :(得分:1)

  

上述解决方案存在一些问题,所以我的工作方式如下!

空值

事实证明,在setDefaultOptions()中,代码:$form->getData['key']返回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有一种机制可以帮助防止跨站点脚本:它们生成一个必须用于表单验证的CSRF令牌。在这个示例中,您没有使用form_rest(表单)显示(因此不提交)它。基本上,form_rest(表单)将"呈现"您之前未呈现的每个字段,但它包含在您传递给视图的表单对象中。 CSRF令牌是其中一个值。

捷希凯

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

  

CSRF令牌无效。请尝试重新提交表单。

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

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

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

  

原来这不是因为我的ajax,而是因为Silex给你一个弃用的DefaultCsrfProvider,它使用会话ID本身作为令牌的一部分,我为了安全起见随机更改了ID。相反,明确地告诉它使用新的CsrfTokenManager修复它,因为那个生成一个令牌并将其存储在会话中,这样会话ID可以改变而不影响令牌的有效性。

因此,我必须删除form.secret选项,并在注册表单提供程序之前将以下内容添加到我的应用程序引导程序

/** 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);
});

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