我目前正面临着SonataAdminBundle的挑战,一对多关系和文件上传。我有一个名为Client
的实体和一个名为ExchangeFile
的实体。一个Client
可以有多个ExchangeFile
,因此我们在这里有一对多的关系。我使用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追加新的表格行),选择另一个文件,然后点击"更新" (保存当前客户端),然后第一个文件不会保留。只能在数据库和文件系统中找到第二个文件。
据我所知,这有以下原因:当绿色" +"第二次点击签名,当前表单发布到Web服务器,包括当前表单中的数据(客户端和所有交换文件)。创建一个新表单并将请求绑定到表单中(这发生在位于AdminHelper
的{{1}}类中):
Sonata\AdminBundle\Admin
因此整个表单被绑定,附加表单行,表单被发送回浏览器,整个表单被新表单覆盖。但由于出于安全原因无法预先填充文件输入(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
}
),因此第一个文件将丢失。当实体被持久化时,该文件仅存储在文件系统上(我认为<input type="file" />
使用了Doctrine的VichUploaderBundle
),但是当附加表单字段行时,这还没有发生。
我的第一个问题是:我如何解决这个问题,或者我应该去哪个方向?我希望以下用例工作:我想创建一个新的客户端,我知道我将上传三个文件。我点击&#34;新客户&#34;,输入客户数据,点击绿色&#34; +&#34;按钮一次,选择第一个文件。然后我点击&#34; +&#34;再次签名,然后选择第二个文件。对于第三个文件也是如此。所有三个文件都应该保留。
第二个问题:当我只想在一对多关系中添加单个表单行时,为什么Sonata Admin会发布整个表单?这真的有必要吗?这意味着如果我有文件输入,则每次添加新表单行时都会上载表单中的所有文件。
提前感谢您的帮助。如果您需要任何细节,请告诉我。
答案 0 :(得分:3)
继续我对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')
;
}
}
答案 1 :(得分:0)
我尝试了许多不同的方法和解决方法,最后我发现了这里描述的最佳解决方案https://stackoverflow.com/a/25154867/4249725
如果不需要,您只需隐藏文件选择周围的所有不必要的列表/删除按钮。
在所有其他直接在表单中选择文件的情况下,您迟早会面临一些其他问题 - 使用表单验证,表单预览等。在所有这些情况下,输入字段将被清除。
因此,使用媒体包和sonata_type_model_list可能是最安全的选择,尽管有很多开销。
我发布它,以防有人按照我搜索的方式搜索解决方案。
我还发现了一些针对此问题的java脚本解决方法。当你点击&#34; +&#34;它基本上改变了文件输入的名称。按钮,然后将其还原。
仍然在这种情况下,如果某些验证失败等,你仍然会有重新显示表单的问题。所以我绝对建议使用媒体包方法。
答案 2 :(得分:0)
我发现,可以通过在AJAX调用添加新行之前记住文件输入内容来解决此问题。这有点hacky,但在我现在对其进行测试时,它就可以正常工作。
我们能够覆盖要编辑的模板-base_edit.html.twig。我已经添加了JavaScript以检测添加按钮上的click事件,并且还添加了添加行后的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]);
}
}