【问题标题】:Sonata Admin one-to-many relationship with file upload (appendFormFieldElement)Sonata Admin 与文件上传的一对多关系(appendFormFieldElement)
【发布时间】:2012-11-30 15:46:56
【问题描述】:

我目前在 SonataAdminBundle、一对多关系和文件上传方面面临挑战。我有一个名为Client 的实体和一个名为ExchangeFile 的实体。一个Client 可以有多个ExchangeFiles,所以我们这里是一对多的关系。我正在使用VichUploaderBundle 进行文件上传。

这是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
}

这是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
}

在我的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
}

这允许在客户端编辑表单中对各种交换文件进行内联编辑。到目前为止效果很好:。

问题

但是有一个注意事项:当我点击一次绿色的“+”号(添加一个新的交换文件表单行)时,然后在我的文件系统中选择一个文件,然后点击“+”号再次(通过 Ajax 追加一个新的表单行),选择另一个文件,然后点击“更新”(保存当前客户端),那么第一个文件不会持久化。在数据库和文件系统中只能找到第二个文件。

据我所知,这有以下原因:当第二次点击绿色的“+”号时,当前表单被发布到网络服务器,包括当前表单中的数据(客户端和所有交换文件)。创建了一个新表单并将请求绑定到表单中(这发生在位于Sonata\AdminBundle\AdminAdminHelper 类中):

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
}

所以整个表单被绑定,一个表单行被附加,表单被发送回浏览器,整个表单被新的表单覆盖。但由于出于安全原因无法预先填充文件输入 (&lt;input type="file" /&gt;),因此第一个文件会丢失。该文件仅在实体被持久化时存储在文件系统中(我认为VichUploaderBundle 使用 Doctrine 的 prePersist 来实现这一点),但是当附加表单字段行时,这还不会发生。

我的第一个问题是:我该如何解决这个问题,或者我应该往哪个方向发展?我希望以下用例能够工作:我想创建一个新客户端,并且我知道我将上传三个文件。我点击“新建客户端”,输入客户端数据,点击绿色“+”按钮一次,选择第一个文件。然后我再次点击“+”号,然后选择第二个文件。第三个文件也一样。这三个文件都应该被持久化。

第二个问题:当我只想在一对多关系中添加单个表单行时,为什么 Sonata 管理员会发布整个表单?这真的有必要吗?这意味着如果我有文件输入,则每次添加新的表单行时都会上传表单中存在的所有文件。

提前感谢您的帮助。如果您需要任何详细信息,请告诉我。

【问题讨论】:

  • 鉴于您已经在使用 SonataAdmin,我建议您使用他们的MediaBundle。这不仅适用于图像 - 您可以将其配置为仅用于文件上传(如果您需要的话)。
  • 感谢@RobMasters 的快速回复。我认为使用 SonataMediaBundle 会给我的应用程序增加很多不必要的复杂性。 MediaBundle 将通过在客户端和交换文件之间添加多对多关系来解决该问题。但这意味着 Exchange 文件可能没有所有者而存在。我正在开发一个允许安全交换具有关键内容的文件的应用程序,因此可能有没有关联客户端的文件可能会导致问题。

标签: php symfony symfony-sonata sonata-admin


【解决方案1】:

关于SonataMediaBundle的进一步评论...

如果你确实走这条路,那么你会想要创建一个类似于以下内容的新实体:

/**
 * @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
}

然后,在您的客户实体中:

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
}

... 和您的 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'
    )
    )
;
}

...最后但同样重要的是,您的 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')
        ;
    }
}

【讨论】:

  • 但这使得文件不与客户端关联是可能的,对吧?无论如何,我会试一试。
  • 我仔细看了一下。为此,我不必使用 SonataMediaBundle。我可以使用任何其他文件类来代替Application\Sonata\MediaBundle\Entity\Media。问题是这种设计允许不与任何客户端关联的文件,我真的想避免这种情况。我认为问题出在其他地方,即在表单字段附加机制中。
  • 别担心,我没有意识到这是您业务逻辑的重要组成部分。尽管由于 SonataMediaBundle 要求您创建自己的具体类,但您可能没有理由无法从您的 Media 实体中添加与 Client 的关系。不过,这显然与上述示例的设置不同。
  • 没错,@RobMasters。但我想当我按照你刚才所说的那样实现这个时,我会面临我已经面临的同样的问题。问题是,在使用 Ajax 时,使用sonata_type_collection 的一对多关系的“多”部分中的文件输入似乎根本不受支持。问题似乎在于如何添加新表单行。如果我没记错的话,它适用于任何可以预填充的输入类型。
  • 其实没有,我认为使用 SonataMediaBundle 可以解决这个问题,这就是我建议它的原因。您不会简单地将“文件”上传到集合中,而是将描述文件的 Media 实体添加到集合中 - 因此在您提交表单之前就已经进行了上传。
【解决方案2】:

我发现,可以通过在 AJAX 调用添加新行之前记住文件输入内容来解决此问题。这有点 hacky,但它正在工作,因为我现在正在测试它。

我们可以覆盖模板进行编辑 - base_edit.html.twig。我已经添加了我的 javascript 来检测添加按钮上的点击事件以及添加行后的 javascript。

我的 sonata_type_collection 字段名为 galleryImages

完整的脚本在这里:

$(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]);
        }
}

【讨论】:

    【解决方案3】:

    我尝试了许多不同的方法和解决方法,最后我发现这里描述的最佳解决方案 https://stackoverflow.com/a/25154867/4249725

    如果不需要,您只需隐藏文件选择周围所有不必要的列表/删除按钮。

    在所有其他直接在表单内选择文件的情况下,您迟早会遇到一些其他问题 - 表单验证、表单预览等。在所有这些情况下,输入字段都将被清除。

    因此,尽管开销很大,但使用媒体包和 sonata_type_model_list 可能是最安全的选择。

    我发布它以防有人以我搜索的方式搜索解决方案。

    我还为这个确切的问题找到了一些 java 脚本解决方法。当您点击“+”按钮然后将其恢复时,它基本上可以更改文件输入的名称。

    在这种情况下,如果某些验证失败等,您仍然会遇到重新显示表单的问题,所以我绝对建议使用媒体捆绑方法。

    【讨论】:

      猜你喜欢
      • 2020-04-10
      • 2013-06-04
      • 1970-01-01
      • 2015-08-12
      • 2014-03-09
      • 2012-07-05
      • 2018-02-09
      • 2016-06-02
      • 2019-04-12
      相关资源
      最近更新 更多