Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Загрузка файлов

Аватар, обложка статьи, документ — посетители постоянно загружают файлы. В Symfony файл приходит как объект UploadedFile: его проверяют, дают ему новое имя и переносят в папку хранения, а в базе сохраняют только имя.

Поле в сущности

#[ORM\Column(length: 255, nullable: true)]
private ?string $image = null;

В базе хранится имя файла, а не сам файл.

Поле в форме

use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\File;
 
$builder->add('imageFile', FileType::class, [
    'label' => 'Обложка',
    'mapped' => false,
    'required' => false,
    'constraints' => [
        new File(
            maxSize: '2M',
            extensions: ['jpg', 'png', 'webp'],
            extensionsMessage: 'Загрузите картинку JPG, PNG или WebP',
        ),
    ],
]);

'mapped' => false означает, что поле не связано со свойством сущности: файл обработает контроллер.

Сохранение

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\File\UploadedFile;
 
#[Route('/blog/new', name: 'blog_new')]
public function new(
    Request $request,
    EntityManagerInterface $em,
    #[Autowire('%kernel.project_dir%/public/uploads')] string $uploadDir,
): Response {
    $post = new Post();
    $form = $this->createForm(PostType::class, $post);
    $form->handleRequest($request);
 
    if ($form->isSubmitted() && $form->isValid()) {
        /** @var UploadedFile|null $file */
        $file = $form->get('imageFile')->getData();
 
        if ($file) {
            $name = bin2hex(random_bytes(16)) . '.' . $file->guessExtension();
            $file->move($uploadDir, $name);
            $post->setImage($name);
        }
 
        $em->persist($post);
        $em->flush();
 
        return $this->redirectToRoute('blog_show', ['id' => $post->getId()]);
    }
 
    return $this->render('blog/new.html.twig', ['form' => $form]);
}

Имя создаётся случайным, а расширение guessExtension() определяет по содержимому файла.

Вывод

{% if post.image %}
    <img src="{{ asset('uploads/' ~ post.image) }}" alt="{{ post.title }}">
{% endif %}

Атрибут enctype="multipart/form-data" функция form_start() добавит сама.

Методы UploadedFile

МетодЧто возвращает
getClientOriginalName()Имя файла на компьютере посетителя
guessExtension()Расширение по содержимому
getMimeType()Тип по содержимому
getSize()Размер в байтах
move($dir, $name)Переносит файл в папку

Отдельный сервис

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

namespace App\Service;
 
class FileUploader
{
    public function __construct(
        #[Autowire('%kernel.project_dir%/public/uploads')]
        private string $dir,
    ) {}
 
    public function upload(UploadedFile $file): string
    {
        $name = bin2hex(random_bytes(16)) . '.' . $file->guessExtension();
        $file->move($this->dir, $name);
 
        return $name;
    }
}

Для больших проектов есть пакет VichUploaderBundle: он связывает файл с сущностью и удаляет его вместе с записью.

Частая ошибка

Нельзя сохранять файл под именем от посетителя: getClientOriginalName() может вернуть shell.php или путь с ../. Файл с расширением .php в общедоступной папке сервер выполнит как программу. Имя всегда создавайте сами, расширение берите из guessExtension(), а список допустимых типов ограничивайте в правилах проверки.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.