Documentação do Symfony2
Renderizada do repositório symfony-docs-pt-BR no Github
Gerenciar o upload de arquivos utilizando entidades do Doctrine não é diferente de manusear qualquer outro upload de arquivo. Em outras palavras, você é livre para mover o arquivo em seu controlador após a manipulação do envio de um formulário. Para exemplos de como fazer isso, veja a página de referência do tipo arquivo.
Se você quiser, também pode integrar o upload de arquivo no ciclo de vida de sua entidade (ou seja, criação, atualização e remoção). Neste caso, como a sua entidade é criada, atualizada e removida pelo Doctrine, o tratamento do upload e da remoção de arquivos será realizado automaticamente (sem precisar fazer nada em seu controlador);
Para fazer este trabalho, você precisa cuidar de uma série de detalhes, que serão abordados neste artigo do cookbook.
Primeiro, crie uma classe Entity simples do Doctrine para você trabalhar:
// src/Acme/DemoBundle/Entity/Document.php
namespace Acme\DemoBundle\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';
}
}
A entidade Document tem um nome e ele é associado a um arquivo. A propriedade path armazena o caminho relativo para o arquivo e é persistida no banco de dados. O getAbsolutePath() é um método de conveniência que retorna o caminho absoluto para o arquivo enquanto o getWebPath() é um método de conveniência que retorna o caminho web, que podem ser utilizados em um template para obter o link do arquivo que foi feito o upload.
Tip
Se não tiver feito isso, você deve ler primeiro a documentação sobre o tipo arquivo file para entender como funciona o processo básico de upload.
Note
Se você estiver usando anotações para especificar as suas regras de validação (como mostrado neste exemplo), certifique-se de que tenha ativado a validação por anotação (veja configuração de validação).
Para lidar com o upload do arquivo no formulário, use um campo “virtual” file. Por exemplo, se você está construindo o seu formulário diretamente em um controlador, ele poderia parecer com o seguinte:
public function uploadAction()
{
// ...
$form = $this->createFormBuilder($document)
->add('name')
->add('file')
->getForm();
// ...
}
Em seguida, crie essa propriedade em sua classe Document e adicione algumas regras de validação:
// src/Acme/DemoBundle/Entity/Document.php
// ...
class Document
{
/**
* @Assert\File(maxSize="6000000")
*/
public $file;
// ...
}
Note
Como você está usando a constraint File, o Symfony2 irá “adivinhar” automaticamente que o campo do formulário é do tipo para upload de arquivos. É por isso que você não tem que defini-lo explicitamente ao criar o formulário acima (->add('file')).
O controlador a seguir mostra como lidar com todo o processo:
use Acme\DemoBundle\Entity\Document;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
// ...
/**
* @Template()
*/
public function uploadAction()
{
$document = new Document();
$form = $this->createFormBuilder($document)
->add('name')
->add('file')
->getForm()
;
if ($this->getRequest()->isMethod('POST')) {
$form->bind($this->getRequest());
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($document);
$em->flush();
$this->redirect($this->generateUrl(...));
}
}
return array('form' => $form->createView());
}
Note
Ao escrever o template, não esqueça de definir o atributo enctype:
<h1>Upload File</h1>
<form action="#" method="post" {{ form_enctype(form) }}>
{{ form_widget(form) }}
<input type="submit" value="Upload Document" />
</form>
O controlador anterior irá persistir automaticamente a entidade Document com o nome submetido, mas ele não fará nada a respeito do arquivo e a propriedade path ficará em branco.
Uma maneira fácil de lidar com o upload do arquivo é movê-lo pouco antes da entidade ser persistida e, em seguida, definir a propriedade path de acordo. Comece chamando o novo método upload() na classe Document, que você vai criar no momento para lidar com o upload do arquivo:
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$document->upload();
$em->persist($document);
$em->flush();
$this->redirect(...);
}
O método upload() irá aproveitar o objeto Symfony\Component\HttpFoundation\File\UploadedFile , que é o retornado após um campo file ser submetido:
public function upload()
{
// the file property can be empty if the field is not required
if (null === $this->file) {
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->file->move(
$this->getUploadRootDir(),
$this->file->getClientOriginalName()
);
// set the path property to the filename where you've saved the file
$this->path = $this->file->getClientOriginalName();
// clean up the file property as you won't need it anymore
$this->file = null;
}
Mesmo esta aplicação funcionando, ela sofre de uma grande falha: E se houver um problema quando a entidade for persistida? O arquivo já teria sido movido para seu local definitivo, apesar da propriedade path da entidade não ter sido persistida corretamente.
Para evitar esses problemas, você deve alterar a implementação de forma que as operações do banco de dados e a cópia do arquivo tornem-se atômicas: se há um problema persistindo a entidade ou se o arquivo não pode ser movido, então nada deve ser feito.
Para fazer isso, você precisa mover o arquivo no mesmo momento em que o Doctrine persistir a entidade no banco de dados. Isto pode ser feito lifecycle da entidade:
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
*/
class Document
{
}
Em seguida, refatore a classe Document para aproveitar esses callbacks:
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
*/
class Document
{
/**
* @ORM\PrePersist()
* @ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->file) {
// do whatever you want to generate a unique name
$filename = sha1(uniqid(mt_rand(), true));
$this->path = $filename.'.'.$this->file->guessExtension();
}
}
/**
* @ORM\PostPersist()
* @ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->file) {
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->file->move($this->getUploadRootDir(), $this->path);
unset($this->file);
}
/**
* @ORM\PostRemove()
*/
public function removeUpload()
{
if ($file = $this->getAbsolutePath()) {
unlink($file);
}
}
}
A classe agora faz tudo o que você precisa: ela gera um nome de arquivo único antes de persistir, move o arquivo depois de persistir e remove o arquivo sempre que a entidade for excluída.
Agora que a cópia do arquivo é tratada atomicamente pela entidade, a chamada $document->upload() deve ser removida do controlador:
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($document);
$em->flush();
$this->redirect(...);
}
Note
Os callbacks dos eventos @ORM\PrePersist() e @ORM\PostPersist() são acionados antes e depois da entidade ser persistida no banco de dados. Por outro lado, a callback dos eventos @ORM\PreUpdate() e @ORM\PostUpdate() são chamadas quando a entidade é atualizada.
Caution
As callbacks PreUpdate e PostUpdate são acionadas somente se houver uma alteração em um dos campos de uma entidade que é persistida. Isto significa que, por padrão, se você modificar apenas a propriedade $file, esses eventos não serão disparados, pois a propriedade não é diretamente persistida via Doctrine. Uma solução seria a utilização de um campo updated que é persistido pelo Doctrine e modificá-lo manualmente quando alterar o arquivo.
Se você quiser usar o id como nome do arquivo, a implementação é ligeiramente diferente pois você precisa salvar a extensão na propriedade path, em vez do nome real:
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
*/
class Document
{
// a property used temporarily while deleting
private $filenameForRemove;
/**
* @ORM\PrePersist()
* @ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->file) {
$this->path = $this->file->guessExtension();
}
}
/**
* @ORM\PostPersist()
* @ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->file) {
return;
}
// 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->file->move(
$this->getUploadRootDir(),
$this->id.'.'.$this->file->guessExtension()
);
unset($this->file);
}
/**
* @ORM\PreRemove()
*/
public function storeFilenameForRemove()
{
$this->filenameForRemove = $this->getAbsolutePath();
}
/**
* @ORM\PostRemove()
*/
public function removeUpload()
{
if ($this->filenameForRemove) {
unlink($this->filenameForRemove);
}
}
public function getAbsolutePath()
{
return null === $this->path
? null
: $this->getUploadRootDir().'/'.$this->id.'.'.$this->path;
}
}
Você vai notar que, neste caso, é necessário um pouco mais de trabalho a fim de remover o arquivo. Antes que seja removido, você deve armazenar o caminho do arquivo (pois ele depende do id). Então, uma vez que o objeto foi totalmente removido do banco de dados, você pode apagar o arquivo com segurança (em PostRemove).