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

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/Entity/Product.php
namespace App\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/Form/ProductType.php
namespace App\Form;

use App\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
    {# templates/product/new.html.twig #}
    <h1>Добавление нового продукта</h1>
    
    {{ form_start(form) }}
        {# ... #}
    
        {{ form_row(form.brochure) }}
    {{ form_end(form) }}
    
  • PHP
    1
    2
    3
    4
    5
    6
    <!-- templates/product/new.html.twig -->
    <h1>Добавление нового продукта</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
49
50
51
52
53
54
55
56
57
// src/Controller/ProductController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use App\Entity\Product;
use App\Form\ProductType;

class ProductController extends Controller
{
    /**
     * @Route("/product/new", name="app_product_new")
     */
    public function new(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 = $this->generateUniqueFileName().'.'.$file->guessExtension();

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

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

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

            return $this->redirect($this->generateUrl('app_product_list'));
        }

        return $this->render('product/new.html.twig', array(
            'form' => $form->createView(),
        ));
    }

    /**
     * @return string
     */
    private function generateUniqueFileName()
    {
        // md5() уменьшает схожесть имён файлов, сгенерированных
        // uniqid(), которые основанный на временных отметках
        return md5(uniqid());
    }
}

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

1
2
3
4
5
# config/services.yaml

# ...
parameters:
    brochures_directory: '%kernel.project_dir%/public/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()) ?>">
        Просмотреть брошюру (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/Service/FileUploader.php
namespace App\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->getTargetDir(), $fileName);

        return $fileName;
    }

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

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

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # config/services.yaml
    services:
        # ...
    
        App\Service\FileUploader:
            arguments:
                $targetDir: '%brochures_directory%'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <!-- 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="App\FileUploader">
            <argument>%brochures_directory%</argument>
        </service>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    // config/services.php
    use App\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/Controller/ProductController.php
use Symfony\Component\HttpFoundation\Request;
use App\Service\FileUploader;

// ...
public function new(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
// src/EventListener/BrochureUploadListener.php
namespace App\EventListener;

use Symfony\Component\HttpFoundation\File\UploadedFile;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use App\Entity\Product;
use App\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)
    {
        // загрузка работает только для сущностей Product
        if (!$entity instanceof Product) {
            return;
        }

        $file = $entity->getBrochure();

        // загружать только новые файлы
        if ($file instanceof UploadedFile) {
            $fileName = $this->uploader->upload($file);
            $entity->setBrochure($fileName);
        }
    }
}

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

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    # config/services.yaml
    services:
        _defaults:
            # ... убедитесь, что включено автомонтирование
            autowire: true
        # ...
    
        App\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
    <!-- 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">
    
        <services>
            <!-- ... убедитесь, что включено автомонтирование -->
            <defaults autowire="true" />
            <!-- ... -->
    
            <service id="App\EventListener\BrochureUploaderListener">
                <tag name="doctrine.event_listener" event="prePersist"/>
                <tag name="doctrine.event_listener" event="preUpdate"/>
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    // config/services.php
    use App\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.