Загрузка и работа с файлами в Symfony - это один из ключевых навыков, который нужно освоить для работы с этим фреймворком.

В этой статье мы с вами рассмотрим основные принципы, как работать с загрузкой файлов.

Конструируем html форму для загрузки файла и ее обработчик

Когда мы говорим о загрузке файла, в первую очередь нам нужно создать html форму, в которую будет загружаться файл и ее обработчик.

Для создания формы мы можем сконструировать ее с помощью Form builder. Вы можете обойтись и без Form Builder, просто этот инструмент упрощает создание html форм и их вывод в Twig шаблоне.

Выглядеть это может примерно вот так:

// src/Form/FileUploadType.php
namespace App\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 FileUploadType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('file', FileType::class, [
                'label' => 'Upload File (PDF file)',
            ]);
    }


    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([]);
    }
}

Кстати, говоря, html-форма для загрузки файла может также и находиться и в шаблоне какого-нибудь фронтенд фреймворка (например, Vue или React), но это уже совсем другой разговор о том, как это можно реализовать и принять загруженный файл.

Далее нам потребуется контроллер и одновременно обработчик, который будет принимать файл отправленный в форме и сохранять его на диск.

// src/Controller/FileUploadController.php
namespace App\Controller;


use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Form\FileUploadType;
use Symfony\Component\HttpFoundation\File\Exception\FileException;


class FileUploadController extends AbstractController
{
    /**
     * @Route("/upload", name="file_upload")
     */
    public function upload(Request $request): Response
    {
        $form = $this->createForm(FileUploadType::class);


        $form->handleRequest($request);


        if ($form->isSubmitted() && $form->isValid()) {
            $file = $form->get('file')->getData();


            if ($file) {
                $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
                $newFilename = $originalFilename.'-'.uniqid().'.'.$file->guessExtension();


                try {
                    $file->move(
                        $this->getParameter('uploads_directory'),
                        $newFilename
                    );
                } catch (FileException $e) {
                    // Обработка исключений, если файл не может быть загружен
                }


                return $this->redirectToRoute('file_upload');
            }
        }


        return $this->render('upload.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}

Конструкцией $form = $this->createForm(FileUploadType::class); мы создаем форму, которую в дальнейшем отобразим в twig шаблоне.

 $form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {

Эта конструкция позволяет проверить отправлена ли была форма и валидны ли данные, которые из нее отправляются. 

Далее мы просто принимаем отправленный файл из запроса и сохраняем в нужную папку на сервере. Это можно сделать с помощью конструкции $file->move

В качестве аргумента для этого метода мы можем указать путь для загрузки файла напрямую, но здесь я решил использовать следующую конструкцию:

$this->getParameter('uploads_directory')

Значение для этого параметра можно указать в

# config/services.yaml
parameters:
    uploads_directory: '%kernel.project_dir%/public/uploads'

В шаблонизаторе Twig форма может выглядеть примерно вот так:

{# templates/upload.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
    <h1>Загрузка файла</h1>
    {{ form_start(form) }}
        {{ form_row(form.file) }}
        <button class="btn">Загрузить</button>
    {{ form_end(form) }}
{% endblock %}

Данные отправляются на тот же роут, где находится сама форма.

Полный курс по работе с файлами в Symfony здесь:

https://webkyrs.info/category/osnovy-raboty-s-failami-v-symfony-na-primere-zagruzki-izobrazhenii


Сохранение в базе данных

Если мы хотим сохранить файл в базе данных, как правило, сохраняется не сам файл, а ссылка на него.

И здесь есть особенность, сохраняется даже не ссылка, а просто имя файла с его расширением.

Почему нельзя сохранить полный путь?

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

Формирование пути до файла должно происходить в сервисах Symfony, а не в записях в базе данных.

В этом случае наш контроллер может выглядеть примерно следующим образом:

public function upload(Request $request): Response
    {
        …
        if ($form->isSubmitted() && $form->isValid()) {
            $file = $form->get('file')->getData();


            if ($file) {
                $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
                $newFilename = $originalFilename.'-'.uniqid().'.'.$file->guessExtension();


                try {
                    $file->move(
                        $this->getParameter('uploads_directory'),
                        $newFilename
                    );


                    // Создание новой сущности Product и сохранение имени файла
                    $product = new Product();
                    $product->setFilename($newFilename);
                    $this->entityManager->persist($product);
                    $this->entityManager->flush();


                } catch (FileException $e) {
                    // Обработка исключений, если файл не может быть загружен
                }


                return $this->redirectToRoute('file_upload');
            }
        }
…
    }


Бандл liip/imagine-bundle

Для упрощения работы с файлами в Symfony можно дополнительно установить специальный бандл

composer require liip/imagine-bundle

Более подробно про работу с этим бандлом можно посмотреть в этом видеокурсе:

https://webkyrs.info/category/osnovy-raboty-s-failami-v-symfony-na-primere-zagruzki-izobrazhenii

Работа с этим бандлом тоже очень большая тема и не укладывается в рамки этого материала.

Вынос логики загрузки в отдельный сервис

Как правило, логику загрузки файлов выносят в отдельный сервис, чтобы ее можно было переиспользовать.

Например, этот сервис может выглядеть примерно вот так:

// src/Service/FileUploader.php
namespace App\Service;


use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use App\Entity\Product;


class FileUploader
{
    private $targetDirectory;
    private $entityManager;


    public function __construct(string $targetDirectory, EntityManagerInterface $entityManager)
    {
        $this->targetDirectory = $targetDirectory;
        $this->entityManager = $entityManager;
    }


    public function uploadFile(UploadedFile $file): ?Product
    {
        $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
        $newFilename = $originalFilename . '-' . uniqid() . '.' . $file->guessExtension();


        try {
            $file->move($this->getTargetDirectory(), $newFilename);


            // Создание новой сущности Product и сохранение имени файла
            $product = new Product();
            $product->setFilename($newFilename);
            $this->entityManager->persist($product);
            $this->entityManager->flush();


            return $product;


        } catch (FileException $e) {
            // Обработка исключений, если файл не может быть загружен
            return null;
        }
    }


    public function getTargetDirectory(): string
    {
        return $this->targetDirectory;
    }
}

Код контроллера при этом заметно сокращается

/**
     * @Route("/upload", name="file_upload")
     */
    public function upload(Request $request): Response
    {
        $form = $this->createForm(FileUploadType::class);


        $form->handleRequest($request);


        if ($form->isSubmitted() && $form->isValid()) {
            $file = $form->get('file')->getData();


            if ($file) {
                $product = $this->fileUploader->uploadFile($file);


                if ($product) {
                    return $this->redirectToRoute('file_upload');
                } else {
                    // Обработка ошибки загрузки файла
                    $this->addFlash('error', 'Ошибка загрузки файла.');
                }
            }
        }


        return $this->render('upload.html.twig', [
            'form' => $form->createView(),
        ]);
    }

Это основы загрузки файлов в Symfony. Здесь есть еще много особенностей и тонкостей, с которыми нам предстоит разобраться в будущем.

Полный курс по работе с файлами в Symfony здесь:

https://webkyrs.info/category/osnovy-raboty-s-failami-v-symfony-na-primere-zagruzki-izobrazhenii

Загружать файлы можно в разные источники и по разному организовать этот процесс, но это уже тема другого разговора. Задача этого материала лишь показать общее представление о загрузке файлов в Symfony.