如何上传文件
不用自己的文件上传,你可以考虑使用VichUploaderBundle。这个bundle提供了所有常见的操作(比如,文件重命名,保存和删除)并且它紧紧的整合了Doctrine ORM,Mongodb ODM, PHPCR ODM 和 Propel。
假设你的应用中有一个Product
实体并且你想要给每一个产品添加一个PDF手册。为此,在你的 Product
实体中添加一个名为 brochure
(小册子)的属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
class Product
{
// ...
/**
* @ORM\Column(type="string")
*
* @Assert\NotBlank(message="Please, upload the product brochure as a PDF file.")
* @Assert\File(mimeTypes={ "application/pdf" })
*/
private $brochure;
public function getBrochure()
{
return $this->brochure;
}
public function setBrochure($brochure)
{
$this->brochure = $brochure;
return $this;
}
} |
这个brochure
列的类型是string
,而不是binary
和 blob
,因为它只是存储PDF的文件名而不是文件内容。
然后,像管理Product
实体的表单中添加一个新的brochure
字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// src/AppBundle/Form/ProductType.php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\FileType;
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('brochure', FileType::class, array('label' => 'Brochure (PDF file)'))
// ...
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Product',
));
}
} |
现在,更新这个模板让它渲染这个表单来显示新的brochure
字段(精确模板代码的添加取决于你应用程序中自定义表单渲染的使用):
1 2 3 4 5 6 7 8 |
{# app/Resources/views/product/new.html.twig #}
<h1>Adding a new product</h1>
{{ form_start(form) }}
{# ... #}
{{ form_row(form.brochure) }}
{{ form_end(form) }} |
1 2 3 4 5 6 |
<!-- app/Resources/views/product/new.html.twig -->
<h1>Adding a new product</h1>
<?php echo $view['form']->start($form) ?>
<?php echo $view['form']->row($form['brochure']) ?>
<?php echo $view['form']->end($form) ?> |
最后,你需要更新控制器处理表单的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// src/AppBundle/Controller/ProductController.php
namespace AppBundle\ProductController;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Httpfoundation\Request;
use AppBundle\Entity\Product;
use AppBundle\Form\ProductType;
class ProductController extends Controller
{
/**
* @Route("/product/new", name="app_product_new")
*/
public function newAction(Request $request)
{
$product = new Product();
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// $file stores the uploaded PDF file
/** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */
$file = $product->getBrochure();
// Generate a unique name for the file before saving it
$fileName = md5(uniqid()).'.'.$file->guessExtension();
// Move the file to the directory where brochures are stored
$file->move(
$this->getParameter('brochures_directory'),
$fileName
);
// Update the 'brochure' property to store the PDF file name
// instead of its contents
$product->setBrochure($fileName);
// ... persist the $product variable or any other work
return $this->redirect($this->generateUrl('app_product_list'));
}
return $this->render('product/new.html.twig', array(
'form' => $form->createView(),
));
}
} |
现在,创建brochures_directory
参数,它在控制器中会被用来指定手册应该被存储的目录:
1 2 3 4 5 |
# app/config/config.yml
# ...
parameters:
brochures_directory: '%kernel.root_dir%/../web/uploads/brochures' |
下面是关于上面控制器中代码重要考虑的事情:
当你表单上传时,这个
brochure
属性包含全部的PDF内容。因为这个属性存储的紧紧是文件名,在持久化更新实体之前你一定要设置新的值;在symfony应用中,上传的文件是一个
UploadedFile
类的对象。这个类提供了处理上传文件最常见的操作方法;一个好的安全实践就是不要信任用户输入的东西。这也适用于被你的访客上传的文件。这个
UploadedFile
类提供了方法去获取原始的文件扩展(getExtension()
),原始文件大小(getClientSize()
)和原始文件名称(getClientOriginalName()
)。然而,它们被认为是不安全的,因为恶意用户可能会篡改信息。这就是为什么它总是更好的去生成一个唯一的名称(unique name)并使用guessExtension()
方法让Symfony根据文件猜出正确的MIME扩展。
你可以使用下面的代码链接到产品的PDF手册:
1 |
<a href="{{ asset('uploads/brochures/' ~ product.brochure) }}">View brochure (PDF)</a> |
1 2 3 |
<a href="<?php echo $view['assets']->getUrl('uploads/brochures/'.$product->getBrochure()) ?>">
View brochure (PDF)
</a> |
当创建一个表单来编辑一个已经被持久化的项时,文件格式类型仍认为是File
实例。因为被持久化实体现在只包含相对文件路径,你首先要把配置中的上传路径与存储的文件名连接上,并创建的新File
类:
1 2 3 4 5 6 |
use Symfony\Component\HttpFoundation\File\File;
// ...
$product->setBrochure(
new File($this->getParameter('brochures_directory').'/'.$product->getBrochure())
); |
创建一个上传服务 ¶
为了避免逻辑在控制器中,让控制器变得很庞大,你可以把上传逻辑提取到一个单独的服务中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// src/AppBundle/FileUploader.php
namespace AppBundle;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class FileUploader
{
private $targetDir;
public function __construct($targetDir)
{
$this->targetDir = $targetDir;
}
public function upload(UploadedFile $file)
{
$fileName = md5(uniqid()).'.'.$file->guessExtension();
$file->move($this->targetDir, $fileName);
return $fileName;
}
} |
然后,把这个类定义为一个服务:
1 2 3 4 5 6 |
# app/config/services.yml
services:
# ...
app.brochure_uploader:
class: AppBundle\FileUploader
arguments: ['%brochures_directory%'] |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd"
>
<!-- ... -->
<service id="app.brochure_uploader" class="AppBundle\FileUploader">
<argument>%brochures_directory%</argument>
</service>
</container> |
1 2 3 4 5 6 7 8 |
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
// ...
$container->setDefinition('app.brochure_uploader', new Definition(
'AppBundle\FileUploader',
array('%brochures_directory%')
)); |
现在你已经准备好,在控制器中使用此服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// src/AppBundle/Controller/ProductController.php
// ...
public function newAction(Request $request)
{
// ...
if ($form->isValid()) {
$file = $product->getBrochure();
$fileName = $this->get('app.brochure_uploader')->upload($file);
$product->setBrochure($fileName);
// ...
}
// ...
} |
使用一个Doctrine监听器 ¶
如果你使用的是Doctrine来存储产品实体,你可以创建一个Doctrine listener(Doctrine监听器)当实体被持久化时自动上传这个文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
// src/AppBundle/EventListener/BrochureUploadListener.php
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use AppBundle\Entity\Product;
use AppBundle\FileUploader;
class BrochureUploadListener
{
private $uploader;
public function __construct(FileUploader $uploader)
{
$this->uploader = $uploader;
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$this->uploadFile($entity);
}
public function preUpdate(PreUpdateEventArgs $args)
{
$entity = $args->getEntity();
$this->uploadFile($entity);
}
private function uploadFile($entity)
{
// upload only works for Product entities
if (!$entity instanceof Product) {
return;
}
$file = $entity->getBrochure();
// only upload new files
if (!$file instanceof UploadedFile) {
return;
}
$fileName = $this->uploader->upload($file);
$entity->setBrochure($fileName);
}
} |
现在,注册这个服务为一个Doctrine监听器:
1 2 3 4 5 6 7 8 9 |
# app/config/services.yml
services:
# ...
app.doctrine_brochure_listener:
class: AppBundle\EventListener\BrochureUploadListener
arguments: ['@app.brochure_uploader']
tags:
- { name: doctrine.event_listener, event: prePersist }
- { name: doctrine.event_listener, event: preUpdate } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd"
>
<!-- ... -->
<service id="app.doctrine_brochure_listener"
class="AppBundle\EventListener\BrochureUploaderListener"
>
<argument type="service" id="app.brochure_uploader"/>
<tag name="doctrine.event_listener" event="prePersist"/>
<tag name="doctrine.event_listener" event="preUpdate"/>
</service>
</container> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// app/config/services.php
use Symfony\Component\DependencyInjection\Reference;
// ...
$definition = new Definition(
'AppBundle\EventListener\BrochureUploaderListener',
array(new Reference('brochures_directory'))
);
$definition->addTag('doctrine.event_listener', array(
'event' => 'prePersist',
));
$definition->addTag('doctrine.event_listener', array(
'event' => 'preUpdate',
));
$container->setDefinition('app.doctrine_brochure_listener', $definition); |
当新的产品(Product)实体持久化时,这个监听器会自动被执行。这个方法,你可以从控制器删除所有上传相关的代码。
当从数据库查询实体时,这个监听器还可以基于路径去创建File
实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// ...
use Symfony\Component\HttpFoundation\File\File;
// ...
class BrochureUploadListener
{
// ...
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$fileName = $entity->getBrochure();
$entity->setBrochure(new File($this->targetPath.'/'.$fileName));
}
} |
添加这些之后,配置这个监听器也监听postLoad
事件。