Дата обновления перевода: 2021-05-27

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

Note

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

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

// src/Entity/Product.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

class Product
{
    // ...

    /**
     * @ORM\Column(type="string")
     */
    private $brochureFilename;

    public function getBrochureFilename()
    {
        return $this->brochureFilename;
    }

    public function setBrochureFilename($brochureFilename)
    {
        $this->brochureFilename = $brochureFilename;

        return $this;
    }
}

Отметьте, что тип столбца brochure - string вместо binary или blob, потому что он просто хранит имя PDF-файла, а не его содержание.

Следующим шагом будет добавление нового поля формы, управляющего сущностью Product. Это должно быть поле FileType для того, чтобы браузеры могли отображать виджет загрузки файла. Для того, чтобы заставить его работать, есть один фокус - добавьте поле формы как “неотображенное”, чтобы Symfony не пыталась получить/установить его значение из связанной сущности:

// src/Form/ProductType.php
namespace App\Form;

use App\Entity\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\File;

class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // ...
            ->add('brochure', FileType::class, [
                'label' => 'Brochure (PDF file)',

                // неотображенное означает, что это поле не ассоциировано ни с одним свойством сущности
                'mapped' => false,

                // сделайте его необязательным, чтобы вам не нужно было повторно загружать PDF-файл
                // каждый раз, когда будете редактировать детали Product
                'required' => false,

                // неотображенные полля не могут определять свою валидацию используя аннотации
                // в ассоциированной сущности, поэтому вы можете использовать органичительные классы PHP
                'constraints' => [
                    new File([
                        'maxSize' => '1024k',
                        'mimeTypes' => [
                            'application/pdf',
                            'application/x-pdf',
                        ],
                        'mimeTypesMessage' => 'Please upload a valid PDF document',
                    ])
                ],
            ])
            // ...
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Product::class,
        ]);
    }
}

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

1
2
3
4
5
6
7
8
{# templates/product/new.html.twig #}
<h1>Adding a new product</h1>

{{ form_start(form) }}
    {# ... #}

    {{ form_row(form.brochure) }}
{{ form_end(form) }}

Наконец, вам нужно обновить код контроллера, обрабатывающего форму:

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

use App\Entity\Product;
use App\Form\ProductType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\String\Slugger\SluggerInterface;

class ProductController extends AbstractController
{
    /**
     * @Route("/product/new", name="app_product_new")
     */
    public function new(Request $request, SluggerInterface $slugger)
    {
        $product = new Product();
        $form = $this->createForm(ProductType::class, $product);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            /** @var UploadedFile $brochureFile */
            $brochureFile = $form->get('brochure')->getData();

            // это условие необходимо, потому что поле 'brochure' не обязательно,
            // поэтому PDF-файл должен быть обработан только после загрузки файла
            if ($brochureFile) {
                $originalFilename = pathinfo($brochureFile->getClientOriginalName(), PATHINFO_FILENAME);
                // это необходимо для безопасного включения имени файла в качестве части URL
                $safeFilename = $slugger->slug($originalFilename);
                $newFilename = $safeFilename.'-'.uniqid().'.'.$brochureFile->guessExtension();

                // Переместите файлв каталог, где хранятся брошюры
                try {
                    $brochureFile->move(
                        $this->getParameter('brochures_directory'),
                        $newFilename
                    );
                } catch (FileException $e) {
                    // ... разберитесь с исключением, если что-то случится во время загрузки файла
                }

                // обновляет свойство 'brochureFilename' для сохранения имени PDF-файла,
                // а не его содержания
                $product->setBrochureFilename($newFilename);
            }

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

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

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

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

1
2
3
4
5
# config/services.yaml

# ...
parameters:
    brochures_directory: '%kernel.project_dir%/public/uploads/brochures'

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

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

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

1
<a href="{{ asset('uploads/brochures/' ~ product.brochureFilename) }}">View brochure (PDF)</a>

Tip

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

use Symfony\Component\HttpFoundation\File\File;
// ...

$product->setBrochureFilename(
    new File($this->getParameter('brochures_directory').'/'.$product->getBrochureFilename())
);

Создание сервиса загрузки

Чтобы избежать логики в контроллерах и не утяжелять их, вы можете извлечь логику загрузки в отдельный сервис:

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

use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\String\Slugger\SluggerInterface;

class FileUploader
{
    private $targetDirectory;
    private $slugger;

    public function __construct($targetDirectory, SluggerInterface $slugger)
    {
        $this->targetDirectory = $targetDirectory;
        $this->slugger = $slugger;
    }

    public function upload(UploadedFile $file)
    {
        $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
        $safeFilename = $this->slugger->slug($originalFilename);
        $fileName = $safeFilename.'-'.uniqid().'.'.$file->guessExtension();

        try {
            $file->move($this->getTargetDirectory(), $fileName);
        } catch (FileException $e) {
            // ... разберитесь с исключением, если что-то случится во время загрузки файла
        }

        return $fileName;
    }

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

Tip

В дополнение к общему классу FileException, существуют другие классы исключений для работы м неудавшимися загрузками файлов: CannotWriteFileException, ExtensionFileException, FormSizeFileException, IniSizeFileException, NoFileException, NoTmpDirFileException, и PartialFileException.

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

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # config/services.yaml
    services:
        # ...
    
        App\Service\FileUploader:
            arguments:
                $targetDirectory: '%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
            https://symfony.com/schema/dic/services/services-1.0.xsd">
        <!-- ... -->
    
        <service id="App\Service\FileUploader">
            <argument>%brochures_directory%</argument>
        </service>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\Service\FileUploader;
    
    return static function (ContainerConfigurator $container) {
        $services = $configurator->services();
    
        $services->set(FileUploader::class)
            ->arg('$targetDirectory', '%brochures_directory%')
        ;
    };
    

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

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

use App\Service\FileUploader;
use Symfony\Component\HttpFoundation\Request;

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

    if ($form->isSubmitted() && $form->isValid()) {
        /** @var UploadedFile $brochureFile */
        $brochureFile = $form->get('brochure')->getData();
        if ($brochureFile) {
            $brochureFileName = $fileUploader->upload($brochureFile);
            $product->setBrochureFilename($brochureFileName);
        }

        // ...
    }

    // ...
}

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

Предыдущие версии этой статьи разъясняли, как обрабатывать загрузку файлов, используя слушателей Doctrine. Однако, это больше не рекомендуется, так как события Doctrine не должны быть использованы для логики вашего домена.

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

В качестве альтернативы вы можете использовать событий, слушателей и подписчиков Symfony. .. VichUploaderBundle: https://github.com/dustin10/VichUploaderBundle

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