Doctrine文件上传处理例子

Doctrine是基于数据库抽像层上的ORM,它可以通过PHP对象轻松访问所有的数据库,例如MYSQL,它支持的PHP最低版本为5.2.3,下面我们一起来看看Doctrine文件上传处理例子,希望文章对各位有帮助.

基本设置,创建一个简单的Doctrine实体类:

  1. // src/Acme/DemoBundle/Entity/Document.php
  2. namespace Acme\DemoBundle\Entity;
  3. use Doctrine\ORM\Mapping as ORM;
  4. use Symfony\Component\Validator\Constraints as Assert;
  5. /**
  6. * @ORM\Entity
  7. */
  8. class Document
  9. {
  10. /**
  11. * @ORM\Id
  12. * @ORM\Column(type="integer")
  13. * @ORM\GeneratedValue(strategy="AUTO")
  14. */
  15. public $id;
  16. /**
  17. * @ORM\Column(type="string", length=255)
  18. * @Assert\NotBlank
  19. */
  20. public $name;
  21. /**
  22. * @ORM\Column(type="string", length=255, nullable=true)
  23. */
  24. public $path;
  25. public function getAbsolutePath()
  26. {
  27. return null === $this->path
  28. ? null
  29. : $this->getUploadRootDir().'/'.$this->path;
  30. }
  31. public function getWebPath()
  32. {
  33. return null === $this->path
  34. ? null
  35. : $this->getUploadDir().'/'.$this->path;
  36. }
  37. protected function getUploadRootDir()
  38. {
  39. // the absolute directory path where uploaded
  40. // documents should be saved
  41. return __DIR__.'/../../../../web/'.$this->getUploadDir();
  42. }
  43. protected function getUploadDir()
  44. {
  45. // get rid of the __DIR__ so it doesn't screw up
  46. // when displaying uploaded doc/image in the view.
  47. return 'uploads/documents';
  48. }
  49. }

该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,他可能是这样的.

  1. public function uploadAction()
  2. {
  3. // ...
  4. $form = $this->createFormBuilder($document)
  5. ->add('name')
  6. ->add('file')
  7. ->getForm();
  8. // ...
  9. }

下一步,创建file这个属性到你的Document类中并且添加一些验证规则:

  1. use Symfony\Component\HttpFoundation\File\UploadedFile;
  2. // ...
  3. class Document
  4. {
  5. /**
  6. * @Assert\File(maxSize="6000000")
  7. */
  8. private $file;
  9. /**
  10. * Sets file.
  11. *
  12. * @param UploadedFile $file
  13. */
  14. public function setFile(UploadedFile $file = null)
  15. {
  16. $this->file = $file;
  17. }
  18. /**
  19. * Get file.
  20. *
  21. * @return UploadedFile
  22. */
  23. public function getFile()
  24. {
  25. return $this->file;
  26. }
  27. }
  28. annotations
  29. Annotations
  30. // src/Acme/DemoBundle/Entity/Document.php
  31. namespace Acme\DemoBundle\Entity;
  32. // ...
  33. use Symfony\Component\Validator\Constraints as Assert;
  34. class Document
  35. {
  36. /**
  37. * @Assert\File(maxSize="6000000")
  38. */
  39. private $file;
  40. // ...
  41. }

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

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

  1. // ...
  2. use Acme\DemoBundle\Entity\Document;
  3. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
  4. use Symfony\Component\HttpFoundation\Request;
  5. // ...
  6. /**
  7. * @Template()
  8. */
  9. public function uploadAction(Request $request)
  10. {
  11. $document = new Document();
  12. $form = $this->createFormBuilder($document)
  13. ->add('name')
  14. ->add('file')
  15. ->getForm();
  16. $form->handleRequest($request);
  17. if ($form->isValid()) {
  18. $em = $this->getDoctrine()->getManager();
  19. $em->persist($document);
  20. $em->flush();
  21. return $this->redirect($this->generateUrl(...));
  22. }
  23. return array('form' => $form->createView());
  24. }

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

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

  1. if ($form->isValid()) {
  2. $em = $this->getDoctrine()->getManager();
  3. $document->upload();
  4. $em->persist($document);
  5. $em->flush();
  6. return $this->redirect(...);
  7. }

这个upload()方法利用UploadedFile对象,是它提交后返回file字段:

  1. public function upload()
  2. {
  3. // the file property can be empty if the field is not required
  4. // 该file属性为空这个属性就不需要了
  5. if (null === $this->getFile()) {
  6. return;
  7. }
  8. // use the original file name here but you should
  9. // sanitize it at least to avoid any security issues
  10. // 这里你应该使用原文件名但是应该至少审核它避免一些安全问题
  11. // move takes the target directory and then the
  12. // target filename to move to
  13. // 将目标文件移动到目标目录
  14. $this->getFile()->move(
  15. $this->getUploadRootDir(),
  16. $this->getFile()->getClientOriginalName()
  17. );
  18. // set the path property to the filename where you've saved the file
  19. // 设置path属性为你保存文件的文件名
  20. $this->path = $this->getFile()->getClientOriginalName();
  21. // clean up the file property as you won't need it anymore
  22. // 清理你不需要的file属性
  23. $this->file = null;
  24. }

使用生命周期回调:

生命周期回调是一种有限的技术,他有一些缺点,如果你想移除Document::getUploadRootDir()方法里的写死的编码__DIR__,最好的方法是开始使用Doctrine listeners,在哪里你将能够注入内核参数,如kernel.root_dir来建立绝对路径.

这种原理工作,他有一个缺陷:也就是说当entity持久化时会有什么问题呢?答:该文件已经转移到了它的最终位置,实体类下的path属性不能够正确的实体化.

如果entity有持久化问题或者文件不能够移动,什么事情也没有发生,为了避免这些问题,你应该改变这种实现方式以便数据库操作和自动删除文件:

  1. /**
  2. * @ORM\Entity
  3. * @ORM\HasLifecycleCallbacks
  4. */
  5. class Document
  6. {
  7. }

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

  1. use Symfony\Component\HttpFoundation\File\UploadedFile;
  2. /**
  3. * @ORM\Entity
  4. * @ORM\HasLifecycleCallbacks
  5. */
  6. class Document
  7. {
  8. private $temp;
  9. /**
  10. * Sets file.
  11. *
  12. * @param UploadedFile $file
  13. */
  14. public function setFile(UploadedFile $file = null)
  15. {
  16. $this->file = $file;
  17. // check if we have an old image path
  18. // 检查如果我们有一个旧的图片路径
  19. if (isset($this->path)) {
  20. // store the old name to delete after the update
  21. $this->temp = $this->path;
  22. $this->path = null;
  23. } else {
  24. $this->path = 'initial';
  25. }
  26. }
  27. /**
  28. * @ORM\PrePersist()
  29. * @ORM\PreUpdate()
  30. */
  31. public function preUpload()
  32. {
  33. if (null !== $this->getFile()) {
  34. // do whatever you want to generate a unique name
  35. // 去生成一个唯一的名称
  36. $filename = sha1(uniqid(mt_rand(), true));
  37. $this->path = $filename.'.'.$this->getFile()->guessExtension();
  38. }
  39. }
  40. /**
  41. * @ORM\PostPersist()
  42. * @ORM\PostUpdate()
  43. */
  44. public function upload()
  45. {
  46. if (null === $this->getFile()) {
  47. return;
  48. }
  49. // if there is an error when moving the file, an exception will
  50. // be automatically thrown by move(). This will properly prevent
  51. // the entity from being persisted to the database on error
  52. //当移动文件发生错误,一个异常move()会自动抛出异常。
  53. //这将阻止实体持久化数据库发生错误。
  54. $this->getFile()->move($this->getUploadRootDir(), $this->path);
  55. // check if we have an old image
  56. if (isset($this->temp)) {
  57. // delete the old image
  58. unlink($this->getUploadRootDir().'/'.$this->temp);
  59. // clear the temp image path
  60. $this->temp = null;
  61. }
  62. $this->file = null;
  63. }
  64. /**
  65. * @ORM\PostRemove()
  66. */
  67. public function removeUpload()
  68. {
  69. $file = $this->getAbsolutePath();
  70. if ($file) {
  71. unlink($file);
  72. }
  73. }
  74. }

如果更改你的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中移除了:

  1. if ($form->isValid()) {
  2. $em = $this->getDoctrine()->getManager();
  3. $em->persist($document);
  4. $em->flush();
  5. return $this->redirect(...);
  6. }

这个@ORM\PrePersist()和@ORM\PostPersist()事件回调:一个是在entity持久化到数据库之前触发,一个是在entity持久化到数据库之后触发。另一方面, @ORM\PreUpdate() 和 @ORM\PostUpdate()事件回调时当实体更新时触发。

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

使用ID作为文件名

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

  1. use Symfony\Component\HttpFoundation\File\UploadedFile;
  2. /**
  3. * @ORM\Entity
  4. * @ORM\HasLifecycleCallbacks
  5. */
  6. class Document
  7. {
  8. private $temp;
  9. /**
  10. * Sets file.
  11. *
  12. * @param UploadedFile $file
  13. */
  14. public function setFile(UploadedFile $file = null)
  15. {
  16. $this->file = $file;
  17. // check if we have an old image path
  18. if (is_file($this->getAbsolutePath())) {
  19. // store the old name to delete after the update
  20. $this->temp = $this->getAbsolutePath();
  21. } else {
  22. $this->path = 'initial';
  23. }
  24. }
  25. /**
  26. * @ORM\PrePersist()
  27. * @ORM\PreUpdate()
  28. */
  29. public function preUpload()
  30. {
  31. if (null !== $this->getFile()) {
  32. $this->path = $this->getFile()->guessExtension();
  33. }
  34. }
  35. /**
  36. * @ORM\PostPersist()
  37. * @ORM\PostUpdate()
  38. */
  39. public function upload()
  40. {
  41. if (null === $this->getFile()) {
  42. return;
  43. }
  44. // check if we have an old image
  45. if (isset($this->temp)) {
  46. // delete the old image
  47. unlink($this->temp);
  48. // clear the temp image path
  49. $this->temp = null;
  50. }
  51. // you must throw an exception here if the file cannot be moved
  52. // so that the entity is not persisted to the database
  53. // which the UploadedFile move() method does
  54. $this->getFile()->move(
  55. $this->getUploadRootDir(),
  56. $this->id.'.'.$this->getFile()->guessExtension()
  57. );
  58. $this->setFile(null);
  59. }
  60. /**
  61. * @ORM\PreRemove()
  62. */
  63. public function storeFilenameForRemove()
  64. {
  65. $this->temp = $this->getAbsolutePath();
  66. }
  67. /**
  68. * @ORM\PostRemove()
  69. */
  70. public function removeUpload()
  71. { //phpfensi.com
  72. if (isset($this->temp)) {
  73. unlink($this->temp);
  74. }
  75. }
  76. public function getAbsolutePath()
  77. {
  78. return null === $this->path
  79. ? null
  80. : $this->getUploadRootDir().'/'.$this->id.'.'.$this->path;
  81. }
  82. }

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