Загрузка файлов
Обработка загрузки файлов — практическая тема экзамена Symfony. Нужно знать класс UploadedFile, валидацию файлов, безопасное хранение и отправку файлов клиенту.
Получение загруженного файла
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
class AvatarController extends AbstractController
{
#[Route('/profile/avatar', name: 'avatar_upload', methods: ['POST'])]
public function upload(Request $request): Response
{
/** @var UploadedFile|null $file */
$file = $request->files->get('avatar');
if ($file === null) {
throw new BadRequestHttpException('No file uploaded');
}
// Check upload was successful
if (!$file->isValid()) {
throw new BadRequestHttpException(
'Upload error: ' . $file->getErrorMessage()
);
}
return $this->json(['status' => 'ok']);
}
}
UploadedFile Methods
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\File\UploadedFile;
/** @var UploadedFile $file */
// File information
$file->getClientOriginalName(); // 'photo.jpg' (user-provided, UNTRUSTED!)
$file->getClientOriginalExtension(); // 'jpg' (from original name, UNTRUSTED!)
$file->getClientMimeType(); // 'image/jpeg' (from browser, UNTRUSTED!)
$file->guessExtension(); // 'jpg' (from file content, TRUSTED)
$file->getMimeType(); // 'image/jpeg' (from file content, TRUSTED)
$file->getSize(); // 102400 (bytes)
$file->getError(); // UPLOAD_ERR_OK (0)
$file->getErrorMessage(); // Human-readable error
$file->isValid(); // true if UPLOAD_ERR_OK
// Temporary file path
$file->getPathname(); // '/tmp/phpXyz123'
$file->getRealPath(); // Resolved real path
// Move file to permanent location
$file->move(
'/path/to/upload/dir', // Target directory
'new-filename.jpg' // New filename (optional)
);
Подвох экзамена:
getClientOriginalName(),getClientOriginalExtension()иgetClientMimeType()— данные ОТ ПОЛЬЗОВАТЕЛЯ. Их НЕЛЬЗЯ доверять! Для определения реального типа файла используйтеgetMimeType()иguessExtension(), которые анализируют содержимое файла. На экзамене обязательно спрашивают разницу.
Таблица trusted vs untrusted
| Метод | Источник | Доверять? |
|---|---|---|
getClientOriginalName() |
Браузер | Нет |
getClientOriginalExtension() |
Из имени файла | Нет |
getClientMimeType() |
Браузер | Нет |
getMimeType() |
Анализ содержимого | Да |
guessExtension() |
Анализ содержимого | Да |
getSize() |
PHP | Да |
Безопасная загрузка
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\String\Slugger\SluggerInterface;
class FileUploader
{
public function __construct(
private readonly string $uploadDirectory,
private readonly SluggerInterface $slugger,
) {
}
public function upload(UploadedFile $file): string
{
// Generate safe, unique filename
$originalFilename = pathinfo(
$file->getClientOriginalName(),
PATHINFO_FILENAME
);
$safeFilename = $this->slugger->slug($originalFilename);
$newFilename = $safeFilename . '-' . uniqid() . '.' . $file->guessExtension();
try {
$file->move($this->uploadDirectory, $newFilename);
} catch (FileException $e) {
throw new \RuntimeException('Failed to upload file: ' . $e->getMessage());
}
return $newFilename;
}
}
Конфигурация сервиса
# config/services.yaml
parameters:
upload_directory: '%kernel.project_dir%/public/uploads'
services:
App\Service\FileUploader:
arguments:
$uploadDirectory: '%upload_directory%'
Валидация файлов
<?php
declare(strict_types=1);
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class UserProfile
{
#[Assert\File(
maxSize: '5M',
mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
mimeTypesMessage: 'Пожалуйста, загрузите изображение (JPEG, PNG, WebP)',
)]
private ?UploadedFile $avatar = null;
// Or use Image constraint for images specifically
#[Assert\Image(
maxSize: '5M',
minWidth: 100,
maxWidth: 4000,
minHeight: 100,
maxHeight: 4000,
allowSquare: true,
allowLandscape: true,
allowPortrait: true,
mimeTypes: ['image/jpeg', 'image/png'],
)]
private ?UploadedFile $photo = null;
}
File vs Image constraints
| Constraint | Проверяет |
|---|---|
#[Assert\File] |
Размер, MIME-тип, расширение |
#[Assert\Image] |
Всё из File + размеры изображения, ratio, формат |
Загрузка в формах
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\File;
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name')
->add('image', FileType::class, [
'label' => 'Product Image',
'mapped' => false, // Not mapped to entity property
'required' => false,
'constraints' => [
new File([
'maxSize' => '2M',
'mimeTypes' => ['image/jpeg', 'image/png'],
'mimeTypesMessage' => 'Please upload a valid image',
]),
],
]);
}
}
<?php
declare(strict_types=1);
// Controller handling file upload form
class ProductController extends AbstractController
{
#[Route('/products/new', name: 'product_new', methods: ['GET', 'POST'])]
public function new(
Request $request,
FileUploader $uploader,
): Response {
$form = $this->createForm(ProductType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var UploadedFile|null $imageFile */
$imageFile = $form->get('image')->getData();
if ($imageFile) {
$filename = $uploader->upload($imageFile);
$product->setImageFilename($filename);
}
// Save product...
$this->addFlash('success', 'Product created!');
return $this->redirectToRoute('product_list');
}
return $this->render('product/new.html.twig', [
'form' => $form,
]);
}
}
Подвох экзамена: При использовании
mapped: falseв FileType, файл НЕ автоматически привязывается к свойству entity. Нужно вручную вызвать$form->get('image')->getData()для получения UploadedFile. Еслиmapped: true— Symfony попытается установить UploadedFile в свойство entity.
Отправка файлов клиенту
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
class DownloadController extends AbstractController
{
// Method 1: file() helper from AbstractController
#[Route('/download/{filename}', name: 'file_download')]
public function download(string $filename): BinaryFileResponse
{
$filePath = $this->getParameter('upload_directory') . '/' . $filename;
// Inline — display in browser
return $this->file($filePath);
// Download — force save dialog
return $this->file($filePath, 'custom-name.pdf', ResponseHeaderBag::DISPOSITION_ATTACHMENT);
}
// Method 2: BinaryFileResponse directly
#[Route('/report/{id}', name: 'report_download')]
public function report(int $id): BinaryFileResponse
{
$filePath = '/path/to/report.pdf';
$response = new BinaryFileResponse($filePath);
$response->setContentDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
'report-' . $id . '.pdf'
);
// Auto-detect Content-Type
$response->headers->set('Content-Type', 'application/pdf');
// Delete file after sending (temporary files)
$response->deleteFileAfterSend(true);
return $response;
}
}
PHP Upload Limits
; php.ini settings that affect file uploads
upload_max_filesize = 10M ; Max size per file
post_max_size = 12M ; Max size of entire POST body (> upload_max_filesize!)
max_file_uploads = 20 ; Max number of files per request
file_uploads = On ; Enable file uploads
Подвох экзамена:
post_max_sizeдолжен быть БОЛЬШЕupload_max_filesize. Еслиpost_max_sizeменьше — загрузка молча провалится и$_FILESбудет пустым. Symfony не может обнаружить эту ситуацию — файл просто будетnullв Request.