> Symfony中文手册 > 如何上传文件

如何上传文件

不用自己的文件上传,你可以考虑使用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,而不是binaryblob ,因为它只是存储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'

下面是关于上面控制器中代码重要考虑的事情:

  1. 当你表单上传时,这个brochure属性包含全部的PDF内容。因为这个属性存储的紧紧是文件名,在持久化更新实体之前你一定要设置新的值;

  2. 在symfony应用中,上传的文件是一个UploadedFile类的对象。这个类提供了处理上传文件最常见的操作方法;

  3. 一个好的安全实践就是不要信任用户输入的东西。这也适用于被你的访客上传的文件。这个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 事件。