Як завантажувати файли

Дата оновлення перекладу 2022-12-14

Як завантажувати файли

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

Відмітьте, що тип стовпчика brochureFilename - string замість binary або blob, тому що він просто зберігає ім'я PDF-файлу, а не його зміст.

Наступним кроком буде додавання нового поля форми, яке керує сутністю Product. Це має бути поле FileType для того, щоб браузери могли відображати віджет завантаження файлу. Для того, щоб змусити його працювати, є один фокус - додайте поле форми як "невідображене", щоб Symfony не намагалася отримати/встановити його значення з пов'язаної сутності:

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/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) }}

Нарешті, вам необхідно оновити код контролера, який обробляє форму:

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
58
59
60
// 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 або будь-яку іншу роботу

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

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

Тепер, створіть параметр brochures_directory, який було використано в контролері, щоб вказати каталог, в якому мають зберігатися брошури:

1
2
3
4
5
# config/services.yaml

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

Варто врахувати деякі важливі речі в коді контролера, описаного вище:

  1. В додатках Symfony, завантажені файли є об'єктами класу UploadedFile. Цей клас надає методи для найбільш розповсюджених операцій при роботі з завантаженими файлами;
  2. Широко відома краща практика безпеки - ніколи не довіряти інформації, яку було введено користувачами. Це також стосується файлів, завантажених вашими відвідувачами. Клас UploadedFile надає методи для отримання оригінального розширення оригінального розміру файлу (getClientSize()) та оригінального імені файлу (getClientOriginalName()). Однак, вони вважаються небезпечними, так як зловмисний користувач міг підробити цю інформацію. Тому завжди краще згенерувати унікальне ім'я та використовувати метод guessExtension(), щоб дати Symfony вгадати правильне розширення у відповідності до MIME-типу файлу;

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

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

Tip

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

1
2
3
4
5
6
use Symfony\Component\HttpFoundation\File\File;
// ...

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

Створення сервісу завантаження

Щоб уникнути логіки в контролерах та не обтяжувати їх, ви можете відокремити логіку завантаження в окремий сервіс:

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
// 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
  • XML
  • PHP
1
2
3
4
5
6
7
# config/services.yaml
services:
    # ...

    App\Service\FileUploader:
        arguments:
            $targetDirectory: '%brochures_directory%'

Тепер ви готові використовувати цей сервіс в контролері:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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.