除了您自己上傳文件,您或許考慮使用 VichUploaderBundle 社區(qū) bundle。這個(gè) bundle 提供了所有常見的操作(例如文件重命名、保存和刪除),并且它緊密地與 Doctrine ORM、MongoDB ODM、PHPCR ODM 和 Propel 組成為一個(gè)整體。
用 Doctrine 實(shí)體上傳文件與上傳任何其他文件無區(qū)別。換句話說,您可以在提交表單之后自由移動(dòng)您控件中的文件。為了舉例如何做這個(gè),參見文件類型引用頁(yè)面。
如果您選擇的話,您也可以整合上傳文件到您的實(shí)體生命周期(例如,創(chuàng)建、更新和移除)。這種情況下,當(dāng)您的實(shí)體被創(chuàng)建,更新或者是從 Doctrine 移除,上傳文件和移除進(jìn)程將會(huì)自動(dòng)發(fā)生(不需要在您的控件中做任何事)。
要使這個(gè)奏效,您需要注意大量的細(xì)節(jié),將會(huì)在這本教程條目中講到。
首先,創(chuàng)建一個(gè)簡(jiǎn)單的 Doctrine 實(shí)體類來使用:
// src/AppBundle/Entity/Document.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
*/
class Document
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
/**
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank
*/
public $name;
/**
* @ORM\Column(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 實(shí)體有一個(gè)名稱并且與一個(gè)文件相關(guān)聯(lián)。path 屬性儲(chǔ)存相關(guān)的路徑到文件,并且保存到數(shù)據(jù)庫(kù)中。
getAbsolutePath() 是一個(gè)可以將絕對(duì)路徑返回到文件的便捷方法,而 getWebPath() 是一個(gè)可以將網(wǎng)頁(yè)路徑返回,可用于模板鏈接上傳文件的便捷方法。
如果您還未做完,您應(yīng)該首先閱讀文件類型文檔來了解基本的上傳進(jìn)程是如何運(yùn)行的。
如果您正在使用標(biāo)注來指定您的驗(yàn)證規(guī)則(正如例子所示),確保您已經(jīng)用標(biāo)注啟動(dòng)了驗(yàn)證(參見驗(yàn)證配置)。
如果您使用方法 getUploadRootDir(),注意這會(huì)保存根文件的內(nèi)部文件,可以被所有人讀取。要考慮把它放在根文件之外,并當(dāng)您需要保護(hù)這些文件的時(shí)候添加自定義查看邏輯。
要上傳表單中的實(shí)際文件,使用一個(gè)“虛擬” file 域。例如,如果您正在一個(gè)控件里直接構(gòu)建您的表單,它看起來會(huì)像這樣:
public function uploadAction()
{
// ...
$form = $this->createFormBuilder($document)
->add('name')
->add('file')
->getForm();
// ...
}
接下來,在您的 Document 類里創(chuàng)建這個(gè)屬性,并添加一些驗(yàn)證規(guī)則:
use Symfony\Component\HttpFoundation\File\UploadedFile;
// ...
class Document
{
/**
* @Assert\File(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
// src/AppBundle/Entity/Document.php
namespace AppBundle\Entity;
// ...
use Symfony\Component\Validator\Constraints as Assert;
class Document
{
/**
* @Assert\File(maxSize="6000000")
*/
private $file;
// ...
}
YAML:
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Document:
properties:
file:
- File:
maxSize: 6000000
XML:
<!-- src/AppBundle/Resources/config/validation.xml -->
<class name="AppBundle\Entity\Document">
<property name="file">
<constraint name="File">
<option name="maxSize">6000000</option>
</constraint>
</property>
</class>
PHP:
// src/AppBundle/Entity/Document.php
namespace Acme\DemoBundle\Entity;
// ...
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints as Assert;
class Document
{
// ...
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint('file', new Assert\File(array(
'maxSize' => 6000000,
)));
}
}
當(dāng)您正在使用 File 約束,Symfony 會(huì)自動(dòng)猜測(cè)表單域是文件上傳輸入。這就是您為什么在創(chuàng)建上面的表單時(shí)(->add('file'))不需要做顯示設(shè)置的原因。
以下控件展示了如何處理整個(gè)進(jìn)程:
// ...
use AppBundle\Entity\Document;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
// ...
/**
* @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->redirectToRoute(...);
}
return array('form' => $form->createView());
}
之前的控件會(huì)用提交的名字自動(dòng)保存 Document 實(shí)體,但是不會(huì)對(duì)文件做任何事情并且 path 屬性為空白。
上傳文件的一個(gè)簡(jiǎn)單的方法是在實(shí)體保存之前移動(dòng)文件,然后相應(yīng)地設(shè)置 path 屬性。首先在 Document 類調(diào)用一個(gè)新的 upload() 方法,您就能立刻上傳文件:
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$document->upload();
$em->persist($document);
$em->flush();
return $this->redirectToRoute(...);
}
upload() 方法會(huì)利用 UploadedFile 對(duì)象,在一個(gè) file 域提交后會(huì)返回:
public function upload()
{
// the file property can be empty if the field is not required
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
$this->path = $this->getFile()->getClientOriginalName();
// clean up the file property as you won't need it anymore
$this->file = null;
}
使用生命周期回呼是一個(gè)限制的技術(shù),有一些缺陷。如果您想移除在 Document::getUploadRootDir() 方法內(nèi)部的硬編碼的 DIR 引用,最好的方法就是開始使用明確的 doctrine 監(jiān)聽器注入內(nèi)核參數(shù),比如 kernel.root_dir 來構(gòu)建絕對(duì)路徑。
盡管這個(gè)實(shí)現(xiàn)奏效,但是它有一個(gè)主要缺陷:如果實(shí)體保存的時(shí)候有問題怎么辦?文件已經(jīng)移動(dòng)到了它的最終位置盡管實(shí)體的 path 屬性未被正確保存。
為了避免這類問題,您應(yīng)該改變實(shí)施從而使數(shù)據(jù)庫(kù)操作和文件的移動(dòng)具有原子性:如果在保存實(shí)體時(shí)有問題或者文件不能被移動(dòng),那么沒有事情會(huì)發(fā)生。
要做到這一點(diǎn),您需要正確移動(dòng)文件因?yàn)?Doctrine 保存實(shí)體到數(shù)據(jù)庫(kù)。這個(gè)可以通過掛鉤一個(gè)實(shí)體生命周期回呼完成。
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
*/
class Document
{
}
接下來,重構(gòu) Document 類來利用這些回呼:
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
*/
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';
}
}
/**
* @ORM\PrePersist()
* @ORM\PreUpdate()
*/
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();
}
}
/**
* @ORM\PostPersist()
* @ORM\PostUpdate()
*/
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
$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;
}
/**
* @ORM\PostRemove()
*/
public function removeUpload()
{
$file = $this->getAbsolutePath();
if ($file) {
unlink($file);
}
}
}
如果對(duì)你實(shí)體的改變被一個(gè) Doctrine 事件監(jiān)聽器或者事件訂閱者所處理,preUpdate() 回呼必須通知 Doctrine 所完成的變化。關(guān)于 preUpadate 事件限制的所有引用,在 Doctrine 事件文檔中參見 preUpdate。
類現(xiàn)在做一切您需要的事情:它會(huì)在保存之前產(chǎn)生一個(gè)獨(dú)特的文件名,在保存之后移動(dòng)文件,并且如果實(shí)體被刪除的話就移除文件。
現(xiàn)在文件的移動(dòng)是由實(shí)體自動(dòng)處理的,$document->upload() 的調(diào)用應(yīng)從控件中移除:
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($document);
$em->flush();
return $this->redirectToRoute(...);
}
@ORM\PrePersist() 和 @ORM\PostPersist() 事件回呼在實(shí)體保存到數(shù)據(jù)庫(kù)前后被觸發(fā)。在另一方面,當(dāng)實(shí)體更新后,@ORM\PreUpdate() 和 @ORM\PostUpdate() 事件回呼被調(diào)用。
如果被保存的實(shí)體的字段其中之一有變化,PreUpdate 和 PostUpdate 回呼才會(huì)被激發(fā)。這意味著,默認(rèn)情況下,如果您只調(diào)整 $file 屬性,這些事件將不再被激發(fā),因?yàn)閷傩员旧聿皇侵苯油ㄟ^ Doctrine 保存的。一個(gè)解決方案就是使用一個(gè)保存在 Doctrine 中的 updated 字段,然后當(dāng)改變文件的時(shí)候手動(dòng)調(diào)整。
如果您想使用 id 作為文件的名稱,操作和您需要在 path 屬性下保存的擴(kuò)展有輕微的不同,并不是實(shí)際的文件名稱:
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
*/
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';
}
}
/**
* @ORM\PrePersist()
* @ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->getFile()) {
$this->path = $this->getFile()->guessExtension();
}
}
/**
* @ORM\PostPersist()
* @ORM\PostUpdate()
*/
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);
}
/**
* @ORM\PreRemove()
*/
public function storeFilenameForRemove()
{
$this->temp = $this->getAbsolutePath();
}
/**
* @ORM\PostRemove()
*/
public function removeUpload()
{
if (isset($this->temp)) {
unlink($this->temp);
}
}
public function getAbsolutePath()
{
return null === $this->path
? null
: $this->getUploadRootDir().'/'.$this->id.'.'.$this->path;
}
}
您將會(huì)注意到在這種情況下,您需要再做一些工作來移除文件。在移除之前,您必須存儲(chǔ)文件路徑(因?yàn)樗Q于 id)。然后,一旦對(duì)象已被完全從數(shù)據(jù)庫(kù)移除,您可以安全地刪除文件(在 PostRemove 中)。