File uploads
An avatar, a post cover, a document — visitors upload files all the time. In Symfony a file arrives as an UploadedFile object: you validate it, give it a new name and move it to a storage folder, while the database keeps only the name.
The entity field
#[ORM\Column(length: 255, nullable: true)]
private ?string $image = null;The database stores the file name, not the file itself.
The form field
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\File;
$builder->add('imageFile', FileType::class, [
'label' => 'Cover',
'mapped' => false,
'required' => false,
'constraints' => [
new File(
maxSize: '2M',
extensions: ['jpg', 'png', 'webp'],
extensionsMessage: 'Upload a JPG, PNG or WebP image',
),
],
]);'mapped' => false means the field is not tied to an entity property: the controller handles the file.
Saving
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]);
}The name is random, and guessExtension() works out the extension from the file's content.
Displaying
{% if post.image %}
<img src="{{ asset('uploads/' ~ post.image) }}" alt="{{ post.title }}">
{% endif %}form_start() adds the enctype="multipart/form-data" attribute by itself.
UploadedFile methods
| Method | What it returns |
|---|---|
getClientOriginalName() | The file name on the visitor's computer |
guessExtension() | The extension, from the content |
getMimeType() | The type, from the content |
getSize() | The size in bytes |
move($dir, $name) | Moves the file into a folder |
A separate service
If files are uploaded in several places, the saving code goes into a service:
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;
}
}Large projects use the VichUploaderBundle package: it ties a file to an entity and deletes it together with the record.
Common mistake
Never save a file under the visitor's name: getClientOriginalName() may return shell.php or a path with ../. A server runs a .php file in a public folder as a program. Always create the name yourself, take the extension from guessExtension(), and limit the allowed types in the validation rules.