Как загружать файлы

Note

Вместо того, чтобы заниматься обработкой файла самостоятельно, вы можете рассмотреть использование общественного пакета VichUploaderBundle. Этот пакет предоставляет все распространённые операции (такие как переименовывание файла, сохранение и удаление) и он тесно интегрирован с Doctrine ORM, MongoDB ODM, PHPCR ODM и Propel.

Представьте, что у вас в приложеии есть сущность Product и что вы хотите добавить PDF-брошюру для каждого продукта. Чтобы сделать это, добавьте новое свойство под названием brochure в сущность Product:

 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-файла, а не его содержание.

Далее, добавьте новое поле brochure к форме, которая управляет сущностью Product:

 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
// src/AppBundle/Form/ProductType.php
namespace AppBundle\Form;

use AppBundle\Entity\Product;
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' => Product::class,
        ));
    }
}

Теперь, обновите шаблон, отображающий форму, чтобы он показывал новое поле brochure (то, какой именно код добавлять, зависит от метода, используемого вашим приложением для настройки отображения формы):

  • Twig
    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) }}
    
  • PHP
    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 хранит загруженный PDF-файл
            /** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */
            $file = $product->getBrochure();

            // Сгенерировать уникальное имя файла перед сохранением
            $fileName = md5(uniqid()).'.'.$file->guessExtension();

            // Переместить файл в каталог, где хранятся брошюры
            $file->move(
                $this->getParameter('brochures_directory'),
                $fileName
            );

            // Обновить свойство "брошюра", чтобы оно хранило имя PDF-файла,
            // а не его содержимое
            $product->setBrochure($fileName);

            // ... сохранить переменную $product или любую другую работу

            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.project_dir%/web/uploads/brochures'

Следует учесть некоторые важные вещи в коде вышеописанного контроллера:

  1. Когда форма загружается, свойство brochure содержит всё содержание PDF-файла. Так как это свойство хранит только имя файла, вы должны установить его новое значение до того, как сохранять изменения сущности;
  2. В приложениях Symfony, загруженные файлы являются объектами класса UploadedFile. Этот класс предоставляет методы для наиболее распространённых операций при работе с загруженными файлами;
  3. Широко известная лучшая практика безопасности - никогда не доверять информации, введеной пользователями. Это также применимо к файлам, загруженным вашими посетителями. Класс UploadedFile предоставляет методы для получения оргинального расширения (getExtension()), оригинального размера файла (getClientSize()) и оригинальгого имени файла (getClientOriginalName()). Однако, они считаются небезопасными, так как злоумышленный пользователь мог подделать эту информацию. Поэтому всегда лучше сгенерировать уникальное имя и использвать метод guessExtension(), чтобы дать Symfony угадать правильное расширение в соответствии с MIME-типом файла;

Вы можете использовать следующий код, чтобы создать ссылку на PDF-брошюру продукта:

  • Twig
    1
    <a href="{{ asset('uploads/brochures/' ~ product.brochure) }}">View brochure (PDF)</a>
    
  • PHP
    1
    2
    3
    <a href="<?php echo $view['assets']->getUrl('uploads/brochures/'.$product->getBrochure()) ?>">
        View brochure (PDF)
    </a>
    

Tip

При создании формы для редактуры уже сохранённого объекта, тип формы файла всё ещё ожидает экземпляр 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
24
25
26
27
28
// src/AppBundle/Service/FileUploader.php
namespace AppBundle\Service;

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;
    }

    public function getTargetDir()
    {
        return $this->targetDir;
    }
}

Далее, определите сервис для этого класса:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # app/config/services.yml
    services:
        # ...
    
        AppBundle\Service\FileUploader:
            arguments:
                $targetDir: '%brochures_directory%'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <!-- app/config/services.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="AppBundle\FileUploader">
            <argument>%brochures_directory%</argument>
        </service>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    // app/config/services.php
    use AppBundle\Service\FileUploader;
    
    $container->autowire(FileUploader::class)
        ->setArgument('$targetDir', '%brochures_directory%');
    

Теперь вы готовы использовать этот сервис в контроллере:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// src/AppBundle/Controller/ProductController.php
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Service\FileUploader;

// ...
public function newAction(Request $request, FileUploader $fileUploader)
{
    // ...

    if ($form->isSubmitted() && $form->isValid()) {
        $file = $product->getBrochure();
        $fileName = $fileUploader->upload($file);

        $product->setBrochure($fileName);

        // ...
    }

    // ...
}

Использование слушателя Doctrine

Если вы используете Doctrine для хранения сущности Продукта, вы можете создать слушателя 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\Service\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)
    {
        // загрузка работает только для сущностей Продукта
        if (!$entity instanceof Product) {
            return;
        }

        $file = $entity->getBrochure();

        // загружать только новые файлы
        if (!$file instanceof UploadedFile) {
            return;
        }

        $fileName = $this->uploader->upload($file);
        $entity->setBrochure($fileName);
    }
}

Теперь, зарегистрируйте этот класс, как слушателя Doctrine:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    # app/config/services.yml
    services:
        _defaults:
            # ... be sure autowiring is enabled
            autowire: true
        # ...
    
        AppBundle\EventListener\BrochureUploadListener:
            tags:
                - { name: doctrine.event_listener, event: prePersist }
                - { name: doctrine.event_listener, event: preUpdate }
    
  • XML
     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">
    
        <!-- ... убедитесь, что включено автомонтирование -->
        <defaults autowire="true" ... />
        <!-- ... -->
    
        <service id="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>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    // app/config/services.php
    use AppBundle\EventListener\BrochureUploaderListener;
    
    $container->autowire(BrochureUploaderListener::class)
        ->addTag('doctrine.event_listener', array(
            'event' => 'prePersist',
        ))
        ->addTag('doctrine.event_listener', array(
            'event' => 'preUpdate',
        ))
    ;
    

Теперь этот слушатель будет автоматически выполняться при сохранении новой сущности Продукта. Таким образом, вы можете удалить всё, относящееся к загрузке, из контроллера.

Tip

Этот слушатель также может создать экземпляр File, основанный на пути, при извлечении сущностей из БД:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// ...
use Symfony\Component\HttpFoundation\File\File;

// ...
class BrochureUploadListener
{
    // ...

    public function postLoad(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();

        if (!$entity instanceof Product) {
            return;
        }

        if ($fileName = $entity->getBrochure()) {
            $entity->setBrochure(new File($this->uploader->getTargetDir().'/'.$fileName));
        }
    }
}

После добавления этих строк, сконфигурируйте слушателя так, чтобы он также слушал событие postLoad.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.