php 131 lines · 4 tabs

Symfony Avatar Upload: Constrained Form Type, Image Validation, and an Uploader Service

Shared by codesnips Jul 2026
4 tabs
<?php

namespace App\Dto;

use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;

class AvatarUploadDto
{
    #[Assert\NotNull(message: 'Please select an image to upload.')]
    #[Assert\Image(
        maxSize: '2M',
        mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
        mimeTypesMessage: 'Only JPEG, PNG or WebP images are allowed.',
        minWidth: 100,
        minHeight: 100,
        maxWidth: 4000,
        maxHeight: 4000,
        minWidthMessage: 'The image must be at least {{ min_width }}px wide.'
    )]
    public ?UploadedFile $file = null;
}
4 files · php Explain with highlit

This snippet shows the idiomatic Symfony way to accept an avatar upload: keep the request bound to a small DTO, validate the file with the built-in Image constraint, and move persistence of the physical file into a dedicated service. Separating these concerns keeps the controller thin and makes the storage logic reusable and testable.

In AvatarUploadDto, the incoming file is modelled as a plain object property annotated with the Image constraint. Rather than validating an entity directly, a DTO isolates the upload from the domain model — the constraint enforces maxSize, an allowed mimeTypes whitelist, and minimum/maximum pixel dimensions, all with custom messages. The NotNull guard ensures the field is actually present. This is important because relying on client-side accept attributes alone is not security: the mimeTypes check inspects the real MIME type detected by Symfony's MimeTypes component, so a renamed .exe will be rejected.

AvatarType is the FormType that binds to the DTO. It sets data_class to the DTO and declares the file field as a FileType that is 'mapped' => true, letting Symfony hydrate the UploadedFile onto the DTO where the constraint lives. Keeping constraints on the DTO instead of the form means the same rules apply whether the data arrives via the form or is validated programmatically.

AvatarUploader is the service that does the real work. upload() derives a collision-resistant name with Slugger plus uniqid(), preserving the original extension guessed from the file, and calls move() into a configured targetDirectory. The directory is injected via a bound parameter, so the path is configuration, not a hardcoded string. move() can throw FileException, which the caller is expected to handle.

ProfileController wires it together: it builds the form, checks isSubmitted() and isValid() (validation runs the DTO constraints automatically), hands the UploadedFile to the uploader, stores the returned filename on the User, and flushes. Note the controller never touches the filesystem itself. A common pitfall this design avoids is trusting the uploaded filename — always regenerate it, since the client controls the original. The trade-off is a little indirection, but the payoff is validation, storage, and HTTP handling that can each evolve independently.


Related snips

Share this code

Here's the card — post it anywhere.

Symfony Avatar Upload: Constrained Form Type, Image Validation, and an Uploader Service — share card
Link copied