简体   繁体   English

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

[英]Forms with Symfony2 - Doctrine Entity with some immutable constructor parameters and OneToMany association

I have a OneToMany association between a Server entity and Client entities in the database. 我在Server实体和数据库中的Client实体之间有一个OneToMany关联。 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. 我想创建一个表单,用户可以从下拉列表中选择服务器,填写新客户端的一些详细信息,然后提交。

Goal 目标

To create a form where a user can input data into fields for the Client , choose a Server from the dropdown, then click submit and have this data (and the association) persisted via Doctrine. 要创建用户可以将数据输入到Client字段的表单,请从下拉列表中选择一个Server ,然后单击“提交”并通过Doctrine保留此数据(和关联)。

Simple, right? 简单吧? Hell no. 一定不行。 We'll get to that. 我们会做到这一点。 Here's the pretty form as it stands: 这是现在的漂亮形式:

服务器客户端表单

Things of note: 注意事项:

  • Server is populated from the Server entities ( EntityRepository::findAll() ) 服务器是从Server实体( EntityRepository::findAll() )填充的
  • Client is a dropdown with hardcoded values 客户端是具有硬编码值的下拉列表
  • Port, endpoint, username and password are all text fields 端口,端点,用户名和密码都是文本字段

Client Entity 客户实体

In my infinite wisdom I have declared that my Client entity has the following constructor signature: 在我无限的智慧中,我宣称我的Client实体具有以下构造函数签名:

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

This will not change . 这不会改变 To create a valid Client object, the above constructor parameters exist. 要创建有效的Client对象,请存在上述构造函数参数。 They are not optional, and this object cannot be created without the above parameters being given upon object instantiation. 它们不是可选的,如果没有在对象实例化时给出上述参数,则无法创建此对象。

Potential Problems : 潜在问题

  • The type property is immutable. type属性是不可变的。 Once you've created a client, you cannot change the type. 创建客户端后,无法更改类型。

  • I do not have a setter for type . 我没有type的setter。 It is a constructor parameter only . 它只是一个构造函数参数 This is because once a client is created, you cannot change the type. 这是因为一旦创建了客户端, 就无法更改类型。 Therefore I am enforcing this at the entity level. 因此,我在实体层面强制执行此操作。 As a result, there is no setType() or changeType() method. 因此,没有setType()changeType()方法。

  • I do not have the standard setObject naming convention. 我没有标准的setObject命名约定。 I state that to change the port, for example, the method name is changePort() not setPort() . 我声明要更改端口,例如,方法名称是changePort()而不是setPort() This is how I require my object API to function, before the use of an ORM. 这是我在使用ORM之前需要我的对象API运行的方式。

Server Entity 服务器实体

I'm using __toString() to concatenate the name and ipAddress members to display in the form dropdown: 我正在使用__toString()来连接nameipAddress成员以在表单下拉列表中显示:

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

Custom Form Type 自定义表单类型

I used Building Forms with Entities as a baseline for my code. 我使用带有实体的构建表单作为我的代码的基线。

Here is the ClientType I created to build the form for me: 这是我为我构建表单而创建的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';
    }
}

The above code is what actually generates the form to be used client-side (via twig). 上面的代码实际上是生成客户端使用的表单(通过twig)。

The Problems 问题

First and foremost, with the above code, submitting the form gives me: 首先,通过上面的代码,提交表单给了我:

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". PropertyAccessor.php第456行中的NoSuchPropertyException :属性“port”和方法之一“addPort()”/“removePort()”,“setPort()”,“port()”,“__ set()”或“ __call()“存在并在类”App \\ Model \\ Entity \\ Client“中具有公共访问权限。

So it can't find the port method. 所以它找不到port方法。 That's because it's changePort() as I explained earlier. 那是因为它是我之前解释过的changePort() How do I tell it that it should use changePort() instead? 我怎么告诉它应该使用changePort()呢? According to the docs I would have to use the entity type for port, endPoint etc. But they're just text fields. 根据文档,我将不得不使用port,endPoint等entity类型。但它们只是文本字段。 How do I go about this the right way? 我该怎么做正确的方法?

I have tried: 我努力了:

  • Setting ['mapped' => false] on port, authUsername etc. This gives me null for all the client fields, but it does seem to have the relevant server details with it. 在port,authUsername等上设置['mapped' => false] 。这使得我对所有客户端字段都为null ,但它似乎确实具有相关的服务器详细信息。 Regardless, $form->isValid() return false . 无论如何, $form->isValid()返回false Here's what var_dump() shows me: 这是var_dump()向我展示的内容:

不 :(

  • A combination of other things involving setting each on field to "entity", and more.. 其他事项的组合涉及将每个字段设置为“实体”,以及更多..

Basically, "it's not working". 基本上,“它不起作用”。 But this is as far as I've got. 但这就是我所拥有的。 What am I doing wrong ? 我做错了什么? I am reading the manual over and over but everything is so far apart that I don't know if I should be using a DataTransformer , the Entity Field Type , or otherwise. 我正在一遍又一遍地阅读手册,但一切都相距甚远,以至于我不知道我是否应该使用DataTransformer实体字段类型或其他方式。 I'm close to scrapping Symfony/Forms altogether and just writing this myself in a tenth of the time. 我已经接近完全废弃Symfony / Forms,只是在十分之一的时间里自己写这个。

Could someone please give me a solid answer on how to get where I want to be? 有人可以给我一个如何到达我想要的地方的坚实答案吗? Also this may help future users :-) 这也可以帮助未来的用户:-)

There are a few problems with the above solution, so here's how I got it working! 上述解决方案存在一些问题,所以我的工作方式就是这样!

Nulls 空值

It turns out that in setDefaultOptions() , the code: $form->getData['key'] was returning null, hence all the nulls in the screenshot. 事实证明,在setDefaultOptions() ,代码: $form->getData['key']返回null,因此屏幕截图中的所有空值。 This needed to be changed to $form->get('key')->getData() 这需要更改为$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()
);

As a result, the data came through as expected, with all the values intact (apart from the id). 结果,数据按预期传出,所有值都保持不变(除了id)。

Twig Csrf Twig Csrf

According to the documentation you can set csrf_protection => false in your form options. 根据文档,您可以在表单选项中设置csrf_protection => false If you don't do this, you will need to render the hidden csrf field in your form: 如果不这样做,则需要在表单中呈现隐藏的csrf字段:

{{ form_rest(form) }}

This renders the rest of the form fields for you, including the hidden _token one: 这将为您呈现其余的表单字段,包括隐藏的_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. Symfony2有一种机制可以帮助防止跨站点脚本:它们生成一个必须用于表单验证的CSRF令牌。 Here, in your example, you're not displaying (so not submitting) it with form_rest(form). 在这里,在您的示例中,您没有使用form_rest(表单)显示(因此不提交)它。 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. 基本上,form_rest(form)将“渲染”您之前未呈现的每个字段,但这些字段包含在您传递给视图的表单对象中。 CSRF token is one of those values. CSRF令牌是其中一个值。

Silex 燧石

Here's the error I was getting after solving the above issue: 这是解决上述问题后我遇到的错误:

The CSRF token is invalid. CSRF令牌无效。 Please try to resubmit the form. 请尝试重新提交表单。

I'm using Silex, and when registering the FormServiceProvider , I had the following: 我正在使用Silex,在注册FormServiceProvider时 ,我有以下内容:

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

This Post shows how Silex is giving you some deprecated CsrfProvider code: 这篇文章展示了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. 原来这不是由于我的ajax,而是因为Silex给你一个弃用的DefaultCsrfProvider,它使用会话ID本身作为令牌的一部分,我为了安全而随机更改ID。 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. 相反,明确地告诉它使用新的CsrfTokenManager修复它,因为那个生成一个令牌并将其存储在会话中,这样会话ID可以改变而不影响令牌的有效性。

As a result, I had to remove the form.secret option and also add the following to my application bootstrap, before registering the form provider : 因此, 在注册表单提供程序之前 ,我必须删除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);
});

With the above modifications, the form now posts and the data is persisted in the database correctly, including the doctrine association! 通过上述修改,表单现在发布,数据正确保存在数据库中,包括学说关联!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM