发布于 2015-08-27 16:52:51 | 175 次阅读 | 评论: 0 | 来源: 网络整理
Handling file uploads with Doctrine entities is no different than handling any other file upload. In other words, you’re free to move the file in your controller after handling a form submission. For examples of how to do this, see the file type reference page.
If you choose to, you can also integrate the file upload into your entity lifecycle (i.e. creation, update and removal). In this case, as your entity is created, updated, and removed from Doctrine, the file uploading and removal processing will take place automatically (without needing to do anything in your controller).
To make this work, you’ll need to take care of a number of details, which will be covered in this cookbook entry.
First, create a simple Doctrine entity class to work with:
// src/AppBundle/Entity/Document.php
namespace AppBundleEntity;
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';
}
}
The Document
entity has a name and it is associated with a file. The path
property stores the relative path to the file and is persisted to the database.
The getAbsolutePath()
is a convenience method that returns the absolute
path to the file while the getWebPath()
is a convenience method that
returns the web path, which can be used in a template to link to the uploaded
file.
小技巧
If you have not done so already, you should probably read the file type documentation first to understand how the basic upload process works.
注解
If you’re using annotations to specify your validation rules (as shown in this example), be sure that you’ve enabled validation by annotation (see validation configuration).
To handle the actual file upload in the form, use a “virtual” file
field.
For example, if you’re building your form directly in a controller, it might
look like this:
public function uploadAction()
{
// ...
$form = $this->createFormBuilder($document)
->add('name')
->add('file')
->getForm();
// ...
}
Next, create this property on your Document
class and add some validation
rules:
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;
}
}
# src/AppBundle/Resources/config/validation.yml
AppBundleEntityDocument:
properties:
file:
- File:
maxSize: 6000000
// src/AppBundle/Entity/Document.php
namespace AppBundleEntity;
// ...
use SymfonyComponentValidatorConstraints as Assert;
class Document
{
/**
* @AssertFile(maxSize="6000000")
*/
private $file;
// ...
}
<!-- src/AppBundle/Resources/config/validation.xml -->
<class name="AppBundleEntityDocument">
<property name="file">
<constraint name="File">
<option name="maxSize">6000000</option>
</constraint>
</property>
</class>
// src/AppBundle/Entity/Document.php
namespace AcmeDemoBundleEntity;
// ...
use SymfonyComponentValidatorMappingClassMetadata;
use SymfonyComponentValidatorConstraints as Assert;
class Document
{
// ...
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint('file', new AssertFile(array(
'maxSize' => 6000000,
)));
}
}
注解
As you are using the File
constraint, Symfony will automatically guess
that the form field is a file upload input. That’s why you did not have
to set it explicitly when creating the form above (->add('file')
).
The following controller shows you how to handle the entire process:
// ...
use AppBundleEntityDocument;
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->redirectToRoute(...);
}
return array('form' => $form->createView());
}
The previous controller will automatically persist the Document
entity
with the submitted name, but it will do nothing about the file and the path
property will be blank.
An easy way to handle the file upload is to move it just before the entity is
persisted and then set the path
property accordingly. Start by calling
a new upload()
method on the Document
class, which you’ll create
in a moment to handle the file upload:
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$document->upload();
$em->persist($document);
$em->flush();
return $this->redirectToRoute(...);
}
The upload()
method will take advantage of the UploadedFile
object, which is what’s returned after a file
field is submitted:
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;
}
警告
Using lifecycle callbacks is a limited technique that has some drawbacks.
If you want to remove the hardcoded __DIR__
reference inside
the Document::getUploadRootDir()
method, the best way is to start
using explicit doctrine listeners.
There you will be able to inject kernel parameters such as kernel.root_dir
to be able to build absolute paths.
Even if this implementation works, it suffers from a major flaw: What if there
is a problem when the entity is persisted? The file would have already moved
to its final location even though the entity’s path
property didn’t
persist correctly.
To avoid these issues, you should change the implementation so that the database operation and the moving of the file become atomic: if there is a problem persisting the entity or if the file cannot be moved, then nothing should happen.
To do this, you need to move the file right as Doctrine persists the entity to the database. This can be accomplished by hooking into an entity lifecycle callback:
/**
* @ORMEntity
* @ORMHasLifecycleCallbacks
*/
class Document
{
}
Next, refactor the Document
class to take advantage of these callbacks:
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
$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);
}
}
}
警告
If changes to your entity are handled by a Doctrine event listener or event
subscriber, the preUpdate()
callback must notify Doctrine about the changes
being done.
For full reference on preUpdate event restrictions, see preUpdate in the
Doctrine Events documentation.
The class now does everything you need: it generates a unique filename before persisting, moves the file after persisting, and removes the file if the entity is ever deleted.
Now that the moving of the file is handled atomically by the entity, the
call to $document->upload()
should be removed from the controller:
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($document);
$em->flush();
return $this->redirectToRoute(...);
}
注解
The @ORMPrePersist()
and @ORMPostPersist()
event callbacks are
triggered before and after the entity is persisted to the database. On the
other hand, the @ORMPreUpdate()
and @ORMPostUpdate()
event
callbacks are called when the entity is updated.
警告
The PreUpdate
and PostUpdate
callbacks are only triggered if there
is a change in one of the entity’s fields that are persisted. This means
that, by default, if you modify only the $file
property, these events
will not be triggered, as the property itself is not directly persisted
via Doctrine. One solution would be to use an updated
field that’s
persisted to Doctrine, and to modify it manually when changing the file.
id
as the Filename¶If you want to use the id
as the name of the file, the implementation is
slightly different as you need to save the extension under the path
property, instead of the actual filename:
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;
}
}
You’ll notice in this case that you need to do a little bit more work in
order to remove the file. Before it’s removed, you must store the file path
(since it depends on the id). Then, once the object has been fully removed
from the database, you can safely delete the file (in PostRemove
).