<?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;
}
<?php
namespace App\Form;
use App\Dto\AvatarUploadDto;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AvatarType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('file', FileType::class, [
'label' => 'Avatar',
'mapped' => true,
'required' => true,
'attr' => [
'accept' => 'image/jpeg,image/png,image/webp',
],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AvatarUploadDto::class,
'csrf_token_id' => 'avatar_upload',
]);
}
}
<?php
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\String\Slugger\SluggerInterface;
class AvatarUploader
{
public function __construct(
#[Autowire('%avatar_directory%')]
private readonly string $targetDirectory,
private readonly SluggerInterface $slugger,
) {
}
public function upload(UploadedFile $file): string
{
$original = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
$safeName = $this->slugger->slug($original)->lower();
$fileName = sprintf('%s-%s.%s', $safeName, uniqid(), $file->guessExtension());
try {
$file->move($this->targetDirectory, $fileName);
} catch (FileException $e) {
throw new FileException('Could not store the uploaded avatar.', 0, $e);
}
return $fileName;
}
}
<?php
namespace App\Controller;
use App\Dto\AvatarUploadDto;
use App\Form\AvatarType;
use App\Service\AvatarUploader;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class ProfileController extends AbstractController
{
#[Route('/profile/avatar', name: 'profile_avatar', methods: ['GET', 'POST'])]
#[IsGranted('ROLE_USER')]
public function avatar(
Request $request,
AvatarUploader $uploader,
EntityManagerInterface $em,
): Response {
$dto = new AvatarUploadDto();
$form = $this->createForm(AvatarType::class, $dto);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$fileName = $uploader->upload($dto->file);
$user = $this->getUser();
$user->setAvatarFilename($fileName);
$em->flush();
$this->addFlash('success', 'Your avatar has been updated.');
return $this->redirectToRoute('profile_avatar');
}
return $this->render('profile/avatar.html.twig', [
'form' => $form->createView(),
]);
}
}
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
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
Share this code
Here's the card — post it anywhere.