Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

MethodWhat 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.

Thanks for reading!

Donate to the site — we work hard for you.