简体   繁体   English

Sonata Admin与文件上传(appendFormFieldElement)一对多关系

[英]Sonata Admin one-to-many relationship with file upload (appendFormFieldElement)

I'm currently facing a challenge with SonataAdminBundle, one-to-many relationships and file uploads. 我目前在SonataAdminBundle,一对多关系和文件上传方面面临挑战。 I have an Entity called Client and one called ExchangeFile . 我有一个称为Client的实体和一个称为ExchangeFile的实体。 One Client can have several ExchangeFile s, so we have a one-to-many relationship here. 一个Client可以有多个ExchangeFile ,因此我们在这里具有一对多的关系。 I'm using the VichUploaderBundle for file uploads. 我正在使用VichUploaderBundle上传文件。

This is the Client class: 这是Client类:

/**
 * @ORM\Table(name="client")
 * @ORM\Entity()
 * @ORM\HasLifecycleCallbacks
 */
class Client extends BaseUser
{    
    // SNIP

    /**
     * @ORM\OneToMany(targetEntity="ExchangeFile", mappedBy="client", orphanRemoval=true, cascade={"persist", "remove"})
     */
    protected $exchangeFiles;

    // SNIP
}

and this is the ExchangeFile class: 这是ExchangeFile类:

/**
 * @ORM\Table(name="exchange_file")
 * @ORM\Entity
 * @Vich\Uploadable
 */
class ExchangeFile
{
    // SNIP

    /**
     * @Assert\File(
     *     maxSize="20M"
     * )
     * @Vich\UploadableField(mapping="exchange_file", fileNameProperty="fileName")
     */
    protected $file;

    /**
     * @ORM\Column(name="file_name", type="string", nullable=true)
     */
    protected $fileName;

    /**
     * @ORM\ManyToOne(targetEntity="Client", inversedBy="exchangeFiles")
     * @ORM\JoinColumn(name="client_id", referencedColumnName="id")
     */
    protected $client;

    // SNIP
}

In my ClientAdmin class, i added the exchangeFiles field the following way: 在我的ClientAdmin类中,我通过以下方式添加了exchangeFiles字段:

protected function configureFormFields(FormMapper $formMapper)
{
    $formMapper
        // SNIP
        ->with('Files')
            ->add('exchangeFiles', 'sonata_type_collection', array('by_reference' => false), array(
                    'edit' => 'inline',
                    'inline' => 'table',
                ))
        // SNIP
}

This allows for inline editing of various exchange files in the Client edit form. 这允许在客户端编辑表单中内联编辑各种交换文件。 And it works well so far: 到目前为止,它运行良好: 具有一对多关系和文件上传的Sonata Admin .

The Problem 问题

But there's one ceveat: When i hit the green "+" sign once (add a new exchange file form row), then select a file in my filesystem , then hit the "+" sign again (a new form row is appended via Ajax), select another file, and then hit "Update" (save the current Client), then the first file is not persisted . 但是有一个提示:当我点击绿色的“ +”号一次(添加一个新的交换文件表单行), 然后在我的文件系统中选择一个文件 ,然后再次点击“ +”号(通过Ajax追加一个新的表单行) ),选择另一个文件,然后点击“更新”(保存当前客户端), 则第一个文件不保留。 Only the second file can be found in the database and the file system. 在数据库和文件系统中只能找到第二个文件。

As far as I could find out, this has the following reason: When the green "+" sign is clicked the second time, the current form is post to the web server, including the data currently in the form (Client and all exchange files). 据我所知,其原因如下:第二次单击绿色的“ +”号时,当前表单被发布到Web服务器,包括当前表单中的数据(客户端和所有交换文件) )。 A new form is created and the request is bound into the form (this happens in the AdminHelper class located in Sonata\\AdminBundle\\Admin ): 创建一个新表单,并将请求绑定到表单中(这在Sonata\\AdminBundle\\Admin中的AdminHelper类中发生):

public function appendFormFieldElement(AdminInterface $admin, $subject, $elementId)
{
    // retrieve the subject
    $formBuilder = $admin->getFormBuilder();

    $form = $formBuilder->getForm();
    $form->setData($subject);
    $form->bind($admin->getRequest()); // <-- here
    // SNIP
}

So the entire form is bound, a form row is appended, the form is sent back to the browser and the entire form is overwritten by the new one. 这样便绑定了整个表单,附加了一个表单行,将该表单发送回浏览器,并且整个表单被新表单覆盖。 But since file inputs ( <input type="file" /> ) cannot be pre-populated for security reasons, the first file is lost. 但是由于出于安全原因不能预先填充文件输入( <input type="file" /> ),因此第一个文件会丢失。 The file is only stored on the filesystem when the entity is persisted (I think VichUploaderBundle uses Doctrine's prePersist for this), but this does not yet happen when a form field row is appended. 仅当实体持久化时文件才存储在文件系统上(我认为VichUploaderBundle使用Doctrine的prePersist ),但是在添加表单字段行时尚未发生这种情况。

My first question is : How can i solve this problem, or which direction should i go? 我的第一个问题是 :我该如何解决这个问题,或者应该朝哪个方向前进? I would like the following use case to work: I want to create a new Client and I know I'll upload three files. 我希望以下用例能够正常工作:我想创建一个新的客户端,并且我知道我将上传三个文件。 I click "New Client", enter the Client data, hit the green "+" button once, select the first file. 我单击“新客户端”,输入客户端数据,单击绿色的“ +”按钮一次,选择第一个文件。 Then i hit the "+" sign again, and select the second file. 然后我再次点击“ +”号,然后选择第二个文件。 Same for the third file. 第三个文件相同。 All three files should be persisted. 所有三个文件都应保留。

Second question : Why does Sonata Admin post the entire form when I only want to add a single form row in a one-to-many relationship? 第二个问题 :当我只想在一对多关系中添加单个表单行时,为什么Sonata Admin发布整个表单? Is this really necessary? 这真的有必要吗? This means that if I have file inputs, all files present in the form are uploaded every time a new form row is added. 这意味着如果我有文件输入,则每次添加新的表单行时,都会上载表单中存在的所有文件。

Thanks in advance for your help. 在此先感谢您的帮助。 If you need any details, let me know. 如果您需要任何详细信息,请告诉我。

Further to my comment about SonataMediaBundle ... 除了我对SonataMediaBundle的评论...

If you do go this route, then you'd want to create a new entity similar to the following: 如果您确实选择这条路线,则需要创建一个类似于以下内容的新实体:

/**
 * @ORM\Table
 * @ORM\Entity
 */
class ClientHasFile
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var Client $client
     *
     * @ORM\ManyToOne(targetEntity="Story", inversedBy="clientHasFiles")
     */
    private $client;

    /**
     * @var Media $media
     *
     * @ORM\ManyToOne(targetEntity="Application\Sonata\MediaBundle\Entity\Media")
     */
    private $media;

    // SNIP
}

Then, in your Client entity: 然后,在您的客户实体中:

class Client
{
    // SNIP

    /**
     * @var \Doctrine\Common\Collections\ArrayCollection
     *
     * @ORM\OneToMany(targetEntity="ClientHasFile", mappedBy="client", cascade={"persist", "remove"}, orphanRemoval=true)
     */
    protected $clientHasFiles;


    public function __construct()
    {
        $this->clientHasFiles = new ArrayCollection();
    }

    // SNIP
}

... and your ClientAdmin's configureFormFields: ...以及您的ClientAdmin的configureFormFields:

protected function configureFormFields(FormMapper $form)
{
    $form

    // SNIP

    ->add('clientHasFiles', 'sonata_type_collection', array(
        'required' => false,
        'by_reference' => false,
        'label' => 'Media items'
    ), array(
        'edit' => 'inline',
        'inline' => 'table'
    )
    )
;
}

... and last but not least, your ClientHasFileAdmin class: ...还有最后但并非最不重要的,您的ClientHasFileAdmin类:

class ClientHasFileAdmin extends Admin
{
    /**
     * @param \Sonata\AdminBundle\Form\FormMapper $form
     */
    protected function configureFormFields(FormMapper $form)
    {
        $form
            ->add('media', 'sonata_type_model_list', array(), array(
                'link_parameters' => array('context' => 'default')
            ))
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function configureListFields(ListMapper $list)
    {
        $list
            ->add('client')
            ->add('media')
        ;
    }
}

I tried many different approaches and workaround and in the end I found out that the best solution in the one described here https://stackoverflow.com/a/25154867/4249725 我尝试了许多不同的方法和解决方法,最后我发现此处所述的最佳解决方案https://stackoverflow.com/a/25154867/4249725

You just have to hide all the unnecessary list/delete buttons around the file selection if they are not needed. 如果不需要,您只需将所有不必要的列表/删除按钮隐藏起来即可。

In all other cases with file selection directly inside the form you will face some other problems sooner or later - with form validation, form preview etc. In all these case input fields will be cleared. 在所有其他直接在表单内部进行文件选择的情况下,您迟早都会遇到其他问题-表单验证,表单预览等。在所有这些情况下,输入字段都将被清除。

So using media bundle and sonata_type_model_list is probably the safest option despite quite a lot of overhead. 因此,尽管有很多开销,但使用媒体捆绑包和sonata_type_model_list可能是最安全的选择。

I'm posting it in case someone is searching for the solution the way I was searching. 我发布它是为了防止有人按照我搜索的方式搜索解决方案。

I've found also some java-script workaround for this exact problem. 我也找到了针对此确切问题的一些Java脚本解决方法。 It worked basically changing names of file inputs when you hit "+" button and then reverting it back. 当您按下“ +”按钮然后将其还原时,它基本上可以更改文件输入的名称。

Still in this case you are still left with the problem of re-displaying the form if some validation fails etc. so I definitely suggest media bundle approach. 仍然在这种情况下,如果某些验证失败等,您仍然面临重新显示表单的问题。因此,我绝对建议您使用媒体捆绑方法。

I've figured out, that it could be possible to solve this problem by remembering the file inputs content before the AJAX call for adding a new row. 我发现,可以通过在AJAX调用添加新行之前记住文件输入内容来解决此问题。 It's a bit hacky, but it's working as I'm testing it right now. 这有点hacky,但在我现在对其进行测试时,它就可以正常工作。

We are able to override a template for editing - base_edit.html.twig. 我们可以覆盖要编辑的模板-base_edit.html.twig。 I've added my javascript to detect the click event on the add button and also a javascript after the row is added. 我已经添加了JavaScript以检测添加按钮上的click事件,并且还添加了添加行后的JavaScript。

My sonata_type_collection field is called galleryImages . 我的sonata_type_collection字段称为galleryImages

The full script is here: 完整的脚本在这里:

$(function(){
      handleCollectionType('galleryImages');
});

function handleCollectionType(entityClass){

        let clonedFileInputs = [];
        let isButtonHandled = false;
        let addButton = $('#field_actions_{{ admin.uniqid }}_' + entityClass + ' a.btn-success');

        if(addButton.length > 0){
            $('#field_actions_{{ admin.uniqid }}_' + entityClass + ' a.btn-success')[0].onclick = null;
            $('#field_actions_{{ admin.uniqid }}_' + entityClass + ' a.btn-success').off('click').on('click', function(e){

                if(!isButtonHandled){
                    e.preventDefault();

                    clonedFileInputs = cloneFileInputs(entityClass);

                    isButtonHandled = true;

                    return window['start_field_retrieve_{{ admin.uniqid }}_'+entityClass]($('#field_actions_{{ admin.uniqid }}_' + entityClass + ' a.btn-success')[0]);
                }
            });

            $(document).on('sonata.add_element', '#field_container_{{ admin.uniqid }}_' + entityClass, function() {
                refillFileInputs(clonedFileInputs);

                isButtonHandled = false;
                clonedFileInputs = [];

                handleCollectionType(entityClass);
            });
        }


}

function cloneFileInputs(entityClass){
        let clonedFileInputs = [];
        let originalFileInputs = document.querySelectorAll('input[type="file"][id^="{{ admin.uniqid }}_' + entityClass + '"]');

        for(let i = 0; i < originalFileInputs.length; i++){
            clonedFileInputs.push(originalFileInputs[i].cloneNode(true));
        }

        return clonedFileInputs;
}

function refillFileInputs(clonedFileInputs){
        for(let i = 0; i < clonedFileInputs.length; i++){
            let originalFileInput = document.getElementById(clonedFileInputs[i].id);
            originalFileInput.replaceWith(clonedFileInputs[i]);
        }
}

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

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