鍍金池/ 教程/ PHP/ 如何用 Doctrine 上傳文件
如何以非繼承方式自定義方法
如何創(chuàng)建事件監(jiān)聽器
如何以非繼承方式擴(kuò)展一個(gè)類
如何記錄消息到不同的文件
如何掌握并創(chuàng)建新的環(huán)境
如何使用 Doctrine DBAL
"XXX is deprecated" E-USER-DEPRECATED 的警告是什么意思?
在登錄表單中使用 CSRF 保護(hù)
如何注冊(cè)自定義 DQL 函數(shù)
如何為表單類配置空數(shù)據(jù)
如何嵌入集合表單
如何創(chuàng)建自定義認(rèn)證提供者
如何使用 Apache Router
如何組織配置文件
部署在 Microsoft Azure 云
如何在路由參數(shù)中允許"/"字符
如何在安全,路由,服務(wù)和驗(yàn)證中使用表達(dá)式
如何對(duì)顯示控制臺(tái)信息配置 Monolog
如何為一個(gè) Bundle 創(chuàng)建友好的配置
如何改變默認(rèn)的目標(biāo)路徑行為
如何在運(yùn)行測(cè)試之前自定義引導(dǎo)過程
如何從路由向控制器傳輸額外的信息
如何從數(shù)據(jù)庫(kù)(實(shí)體提供者)讀取安全用戶
如何從 Controller 調(diào)用一個(gè)命令
如何創(chuàng)建自定義表單密碼驗(yàn)證器
如何使用內(nèi)建的 PHP Web 服務(wù)器
如何在功能測(cè)試中用 Token 模擬認(rèn)證
配置 Session 文件的保存目錄
理解前端控制器、內(nèi)核及環(huán)境如何協(xié)同工作
如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單的注冊(cè)表單
如何使用 Doctrine 擴(kuò)展:Timestampable, Sluggable, Translatable 等等
如何使用多個(gè)實(shí)體管理器和連接
如何自定義表單渲染
如何安裝第三方 Bundles
使用預(yù)認(rèn)證安全防火墻
如何簡(jiǎn)化多個(gè) Bundle 的配置
會(huì)話代理實(shí)例
安裝 Composer
如何冒充一個(gè)用戶
如何注冊(cè)一個(gè)新的請(qǐng)求格式和 MIME 類型
如何在功能測(cè)試中使用分析器
如何在服務(wù)容器內(nèi)設(shè)置外部參數(shù)
如何重寫 Symfony 默認(rèn)的目錄結(jié)構(gòu)
如何在一個(gè) Symfony 控制器中創(chuàng)建一個(gè) SOAP 的 Web 服務(wù)
如何使用序列化
部署在 Platform.sh
升級(jí)一個(gè)副版本
如何寫一個(gè)自定義的 Twig 擴(kuò)展
如何在 SubVersion 中創(chuàng)建并保存一個(gè) Symfony 項(xiàng)目
使用 PHP 庫(kù)聯(lián)合,編譯和最小化 Web 資產(chǎn)
如何創(chuàng)建一個(gè)自定義的數(shù)據(jù)收集器
如何使用和注冊(cè)命名空間路徑
如何使用 Monolog 記錄日志
如何建立一個(gè)傳統(tǒng)的登錄表單
如何強(qiáng)制路由總是使用 HTTPS 或者 HTTP
如何在模板中使用 PHP 而不是 Twig
如何動(dòng)態(tài)選擇密碼加密算法
避免匿名用戶開始 Session 會(huì)話
如何測(cè)試 Doctrine 倉(cāng)庫(kù)
如何在功能測(cè)試中測(cè)試一封電子郵件被發(fā)送
Symfony2 與 Symfony1 的區(qū)別
使用結(jié)尾反斜線重定向 URL
(configuration)如何在數(shù)據(jù)庫(kù)中使用 PdoSessionHandler 存儲(chǔ) Sessions
如何使用匹配器有條件地啟用分析器
部署在 Heroku 云
如何不用自定義控制器配置重定向
如何在 Bundle 內(nèi)部加載服務(wù)配置
如何處理不同的錯(cuò)誤級(jí)別
如何在應(yīng)用中保護(hù)服務(wù)和方法
如何對(duì)表單單元測(cè)試
如何把命令定義為服務(wù)
如何配置 Monolog 從日志中排除 404 錯(cuò)誤
如何使用控制臺(tái)
如何測(cè)試與數(shù)據(jù)庫(kù)交互的代碼
如何在路由中使用除了 GET 和 POST 的 HTTP 方法
如何使用云服務(wù)發(fā)送電子郵件
如何創(chuàng)建一個(gè)控制臺(tái)命令
在遺留的應(yīng)用上使用 Symfony Session
如何使用高級(jí)的訪問控制列表
如何不用一個(gè)自定義的控制器渲染一個(gè)模板
如何重寫部分 Bundle
升級(jí)一個(gè)主版本
安全訪問控制是如何工作的
如何使用 Bundle 的繼承來重寫部分 Bundle
如何使用 Voter 檢查用戶權(quán)限
如何為多個(gè) Doctrine 的實(shí)現(xiàn)提供模型類
如何使用作用域
如何部署一個(gè) Symfony 應(yīng)用
如何用 "inherit-data" 減少代碼冗余
如何注冊(cè)事件監(jiān)聽器和訂閱
使用 Bower 安裝 Symfony
如何創(chuàng)建一個(gè)自定義路由加載器
如何創(chuàng)建一個(gè)自定義的驗(yàn)證限制
在獨(dú)立注入類中使用參數(shù)
如何使用 Assetic 和 Twig Functions 進(jìn)行圖像優(yōu)化
如何利用表單事件動(dòng)態(tài)修改表單
如何在過濾器的前后設(shè)置事件分發(fā)器
如何在 Bundle 中使用 Compiler Passes
緩存包含 CSRF 保護(hù)表單的頁(yè)面
如何注入變量到所有的模板(如全局變量)
如何創(chuàng)建一個(gè)自定義表單域類型
如何限定防火墻使其只允許通過指定請(qǐng)求
如何把 Controller 定義為服務(wù)
如何使用 Gmail 發(fā)送郵件
升級(jí)一個(gè)補(bǔ)丁版本
如何創(chuàng)建一個(gè)表單類型擴(kuò)展
如何對(duì)不同的 URL 強(qiáng)制使用 HTTPS 或者 HTTP
如何使用 Varnish 加速我的 Web 站點(diǎn)
如何定義虛擬類和接口之間的關(guān)系
如何自定義登錄表單
如何測(cè)試多個(gè)客戶端的交互
PSR-7 Bridge
如何使用 YUI Compressor 裁剪 Javascripts 和 Stylesheets
在用戶的 Session 中使用局部 "Sticky"
如何定制錯(cuò)誤頁(yè)
如何從已存在的數(shù)據(jù)庫(kù)中生成實(shí)體
如何用 Doctrine 上傳文件
可復(fù)用 Bundles 的最佳實(shí)踐
如何發(fā)送一封電子郵件
如何將 Assetic Filter 應(yīng)用到具體的文件擴(kuò)展名上
切換分析器存儲(chǔ)
如何上傳文件
限制 Session 元數(shù)據(jù)的寫入
如何使用多用戶提供者
如何使用數(shù)據(jù)轉(zhuǎn)換
配置一個(gè) Web 服務(wù)器
如何編程訪問分析器數(shù)據(jù)
如何在路由中使用服務(wù)容器參數(shù)
如何在控制臺(tái)生成 URL 和發(fā)送郵件
如何將你的開發(fā)環(huán)境優(yōu)化為調(diào)試環(huán)境
如何在控制臺(tái)命令中啟用日志
如何在功能測(cè)試中模擬 HTTP 認(rèn)證
如何使用 API 驗(yàn)證用戶
如何移除 AcmeDemoBundle
控制臺(tái)命令
如何配置 Symfony 使其工作在負(fù)載均衡和反轉(zhuǎn)代理
如何添加“記住我”登錄功能
如何使用 Assetic 進(jìn)行資產(chǎn)管理
如何限定防火墻使其接受指定主機(jī)
如何使用訪問控制列表(ACLs)
如何裁剪 CSS/JS 文件(使用 UglifyJS 和 UglifyCSS)
如何緩存電子郵件
如何使用 submit() 函數(shù)處理表單提交
如何在 Git 中創(chuàng)建并保存一個(gè) Symfony 項(xiàng)目
如何創(chuàng)建自定義用戶提供者
如何對(duì)電子郵件錯(cuò)誤配置 Monolog
如何在開發(fā)時(shí)使用電子郵件

如何用 Doctrine 上傳文件

除了您自己上傳文件,您或許考慮使用 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ì)在這本教程條目中講到。

基本設(shè)置

首先,創(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í)體的字段其中之一有變化,PreUpdatePostUpdate 回呼才會(huì)被激發(fā)。這意味著,默認(rèn)情況下,如果您只調(diào)整 $file 屬性,這些事件將不再被激發(fā),因?yàn)閷傩员旧聿皇侵苯油ㄟ^ Doctrine 保存的。一個(gè)解決方案就是使用一個(gè)保存在 Doctrine 中的 updated 字段,然后當(dāng)改變文件的時(shí)候手動(dòng)調(diào)整。

使用 id 作為文件名稱

如果您想使用 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 中)。