Doctrine文件上传处理例子

作者:袖梨 2022-06-24

基本设置


创建一个简单的Doctrine实体类:


// src/Acme/DemoBundle/Entity/Document.php
namespace AcmeDemoBundleEntity;
 
use DoctrineORMMapping as ORM;
use SymfonyComponentValidatorConstraints as Assert;
 
/**
 * @ORMEntity
 */
class Document
{
    /**
     * @ORMId
     * @ORMColumn(type="integer")
     * @ORMGeneratedValue(strategy="AUTO")
     */
    public $id;
 
    /**
     * @ORMColumn(type="string", length=255)
     * @AssertNotBlank
     */
    public $name;
 
    /**
     * @ORMColumn(type="string", length=255, nullable=true)
     */
    public $path;
 
    public function getAbsolutePath()
    {
        return null === $this->path
            ? null
            : $this->getUploadRootDir().'/'.$this->path;
    }
 
    public function getWebPath()
    {
        return null === $this->path
            ? null
            : $this->getUploadDir().'/'.$this->path;
    }
 
    protected function getUploadRootDir()
    {
        // the absolute directory path where uploaded
        // documents should be saved
        return __DIR__.'/../../../../web/'.$this->getUploadDir();
    }
 
    protected function getUploadDir()
    {
        // get rid of the __DIR__ so it doesn't screw up
        // when displaying uploaded doc/image in the view.
        return 'uploads/documents';
    }
}

该document实体有一个名称与文件相关联。这个path属性存储一个文件的相对路径并且在数据库中存储。这个getAbsolutePath()会返回一个绝对路径,getWebPath()会返回一个web路径,用于模板加入上传文件链接。


如果你还没有这样做的话,你应该阅读http://symfony.com/doc/current/reference/forms/types/file.html首先了解基本的上传过程。

如果您使用注释来验证规则(如本例所示),请确保你启用了注释验证(见http://symfony.com/doc/current/book/validation.html#book-validation-configuration)。

 

在处理一个实际的文件上传时,使用一个“虚拟”的file字段。例如,如果你在controller中直接构建一个form,他可能是这样的:


public function uploadAction()
{
    // ...
 
    $form = $this->createFormBuilder($document)
        ->add('name')
        ->add('file')
        ->getForm();
 
    // ...
}
下一步,创建file这个属性到你的Document类中并且添加一些验证规则:
use SymfonyComponentHttpFoundationFileUploadedFile;
 
// ...
class Document
{
    /**
     * @AssertFile(maxSize="6000000")
     */
    private $file;
 
    /**
     * Sets file.
     *
     * @param UploadedFile $file
     */
    public function setFile(UploadedFile $file = null)
    {
        $this->file = $file;
    }
 
    /**
     * Get file.
     *
     * @return UploadedFile
     */
    public function getFile()
    {
        return $this->file;
    }
}
annotations
Annotations
// src/Acme/DemoBundle/Entity/Document.php
namespace AcmeDemoBundleEntity;
 
// ...
use SymfonyComponentValidatorConstraints as Assert;
 
class Document
{
    /**
     * @AssertFile(maxSize="6000000")
     */
    private $file;
 
    // ...
}

 当你使用File约束,symfony会自动猜测表单字段输入的是一个文件上传。这就是当你创建表单(->add(‘file’))时,为什么没有在表单明确设置为文件上传的原因。

下面的控制器,告诉您如何处理全部过程:


// ...
use AcmeDemoBundleEntityDocument;
use SensioBundleFrameworkExtraBundleConfigurationTemplate;
use SymfonyComponentHttpFoundationRequest;
// ...
 
/**
 * @Template()
 */
public function uploadAction(Request $request)
{
    $document = new Document();
    $form = $this->createFormBuilder($document)
        ->add('name')
        ->add('file')
        ->getForm();
 
    $form->handleRequest($request);
 
    if ($form->isValid()) {
        $em = $this->getDoctrine()->getManager();
 
        $em->persist($document);
        $em->flush();
 
        return $this->redirect($this->generateUrl(...));
    }
 
    return array('form' => $form->createView());
}

以前的controller当提交name自动的存储Document实体,但是他不会做任何关于文件的事情并且path属性也将是空白。


处理文件上传一个简单的方法就是在entity持久化之前设置相应的path属性。在某一时刻处理文件上传时,要调用Document实体类一个upload()方法给path赋值。


if ($form->isValid()) {
    $em = $this->getDoctrine()->getManager();
 
    $document->upload();
 
    $em->persist($document);
    $em->flush();
 
    return $this->redirect(...);
}
这个upload()方法利用UploadedFile对象,是它提交后返回file字段:
public function upload()
{
    // the file property can be empty if the field is not required
    // 该file属性为空这个属性就不需要了
    if (null === $this->getFile()) {
        return;
    }
 
    // use the original file name here but you should
    // sanitize it at least to avoid any security issues
    //  这里你应该使用原文件名但是应该至少审核它避免一些安全问题
    // move takes the target directory and then the
    // target filename to move to
    // 将目标文件移动到目标目录
    $this->getFile()->move(
        $this->getUploadRootDir(),
        $this->getFile()->getClientOriginalName()
    );
 
    // set the path property to the filename where you've saved the file
    // 设置path属性为你保存文件的文件名
    $this->path = $this->getFile()->getClientOriginalName();
 
    // clean up the file property as you won't need it anymore
    // 清理你不需要的file属性
    $this->file = null;
}
 使用生命周期回调
生命周期回调是一种有限的技术,他有一些缺点。如果你想移除Document::getUploadRootDir()方法里的写死的编码__DIR__,最好的方法是开始使用Doctrine listeners。在哪里你将能够注入内核参数,如kernel.root_dir来建立绝对路径。
这种原理工作,他有一个缺陷:也就是说当entity持久化时会有什么问题呢?答:该文件已经转移到了它的最终位置,实体类下的path属性不能够正确的实体化。
(如果entity有持久化问题或者文件不能够移动,什么事情也没有发生)为了避免这些问题,你应该改变这种实现方式以便数据库操作和自动删除文件:
/**
 * @ORMEntity
 * @ORMHasLifecycleCallbacks
 */
class Document
{
}


接下来,利用这些回调函数重构Document类:


use SymfonyComponentHttpFoundationFileUploadedFile;
 
/**
 * @ORMEntity
 * @ORMHasLifecycleCallbacks
 */
class Document
{
    private $temp;
 
    /**
     * Sets file.
     *
     * @param UploadedFile $file
     */
    public function setFile(UploadedFile $file = null)
    {
        $this->file = $file;
        // check if we have an old image path
        // 检查如果我们有一个旧的图片路径
        if (isset($this->path)) {
            // store the old name to delete after the update
            $this->temp = $this->path;
            $this->path = null;
        } else {
            $this->path = 'initial';
        }
    }
 
    /**
     * @ORMPrePersist()
     * @ORMPreUpdate()
     */
    public function preUpload()
    {
        if (null !== $this->getFile()) {
            // do whatever you want to generate a unique name
            // 去生成一个唯一的名称
            $filename = sha1(uniqid(mt_rand(), true));
            $this->path = $filename.'.'.$this->getFile()->guessExtension();
        }
    }
 
    /**
     * @ORMPostPersist()
     * @ORMPostUpdate()
     */
    public function upload()
    {
        if (null === $this->getFile()) {
            return;
        }
 
        // if there is an error when moving the file, an exception will
        // be automatically thrown by move(). This will properly prevent
        // the entity from being persisted to the database on error
        //当移动文件发生错误,一个异常move()会自动抛出异常。
        //这将阻止实体持久化数据库发生错误。
        $this->getFile()->move($this->getUploadRootDir(), $this->path);
 
        // check if we have an old image
        if (isset($this->temp)) {
            // delete the old image
            unlink($this->getUploadRootDir().'/'.$this->temp);
            // clear the temp image path
            $this->temp = null;
        }
        $this->file = null;
    }
 
    /**
     * @ORMPostRemove()
     */
    public function removeUpload()
    {
        $file = $this->getAbsolutePath();
        if ($file) {
            unlink($file);
        }
    }
}

 如果更改你的entity是由Doctrine event listener 或event subscriber处理,这个 preUpdate()回调函数必须通知Doctrine关于正在做的改变。有关preUpdate事件限制的完整参考请查看 http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#preupdate

现在这个类做了你需要的一切:他会在entity持久化之前生成一个唯一的文件名,持久化之后,移动文件,删除文件。


现在移动文件是entity自动完成,这个$document->upload()就应该从controller中移除了:


if ($form->isValid()) {
    $em = $this->getDoctrine()->getManager();
 
    $em->persist($document);
    $em->flush();
 
    return $this->redirect(...);
}

 这个@ORMPrePersist()和@ORMPostPersist()事件回调:一个是在entity持久化到数据库之前触发,一个是在entity持久化到数据库之后触发。另一方面, @ORMPreUpdate() 和 @ORMPostUpdate()事件回调时当实体更新时触发。

当改变entity字段后进行持久化操作时,PreUpdate和PostUpdate回调才会被触发。这意味着,默认情况下,你只改变了$file属性,这些事件不会被触发,因为这个属性它自己不会持久化到Doctrine。有一个解决方法,就是创建一个updated字段把它持久化到Doctrine,并当文件改变时手动调整它。

使用ID作为文件名

如果要使用ID作为文件名,实现略有不同,您需要保存path属性为文件扩展名,而不是实际的文件名:


use SymfonyComponentHttpFoundationFileUploadedFile;
 
/**
 * @ORMEntity
 * @ORMHasLifecycleCallbacks
 */
class Document
{
    private $temp;
 
    /**
     * Sets file.
     *
     * @param UploadedFile $file
     */
    public function setFile(UploadedFile $file = null)
    {
        $this->file = $file;
        // check if we have an old image path
        if (is_file($this->getAbsolutePath())) {
            // store the old name to delete after the update
            $this->temp = $this->getAbsolutePath();
        } else {
            $this->path = 'initial';
        }
    }
 
    /**
     * @ORMPrePersist()
     * @ORMPreUpdate()
     */
    public function preUpload()
    {
        if (null !== $this->getFile()) {
            $this->path = $this->getFile()->guessExtension();
        }
    }
 
    /**
     * @ORMPostPersist()
     * @ORMPostUpdate()
     */
    public function upload()
    {
        if (null === $this->getFile()) {
            return;
        }
 
        // check if we have an old image
        if (isset($this->temp)) {
            // delete the old image
            unlink($this->temp);
            // clear the temp image path
            $this->temp = null;
        }
 
        // you must throw an exception here if the file cannot be moved
        // so that the entity is not persisted to the database
        // which the UploadedFile move() method does
        $this->getFile()->move(
            $this->getUploadRootDir(),
            $this->id.'.'.$this->getFile()->guessExtension()
        );
 
        $this->setFile(null);
    }
 
    /**
     * @ORMPreRemove()
     */
    public function storeFilenameForRemove()
    {
        $this->temp = $this->getAbsolutePath();
    }
 
    /**
     * @ORMPostRemove()
     */
    public function removeUpload()
    {
        if (isset($this->temp)) {
            unlink($this->temp);
        }
    }
 
    public function getAbsolutePath()
    {
        return null === $this->path
            ? null
            : $this->getUploadRootDir().'/'.$this->id.'.'.$this->path;
    }
}

你会注意到,在这种情况下,你需要做一点工作,以删除该文件。在数据删除之前,你必须保存文件路径(因为它依赖于ID)。然后,一旦对象已经完全从数据库中删除,你就可以安全的删除文件(在数据删除之后)。


相关文章

精彩推荐