Symfony Interview Questions: Top 25 in 2026
The 25 most asked Symfony interview questions. Architecture, Doctrine ORM, services, security, forms, and testing with detailed answers and code examples.

Symfony interviews assess mastery of the reference professional PHP framework, understanding of component-oriented architecture, Doctrine ORM, and the ability to build robust and scalable applications. This guide covers the 25 most asked questions, from Symfony fundamentals to advanced production patterns.
Recruiters appreciate candidates who understand Symfony's philosophy: decoupling via services, explicit configuration, and PSR standards compliance. Explaining the framework's architectural choices makes the difference.
Symfony Fundamentals
Question 1: Explain the Request Lifecycle in Symfony
Symfony's request lifecycle traverses the HTTP Kernel and uses an event system to allow extension at each step. Understanding this is essential for debugging and customizing application behavior.
// Entry point for all HTTP requests
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
// Symfony Runtime handles the bootstrap
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};// The Kernel orchestrates request processing
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
// The Kernel loads bundles and configures the container
// Key lifecycle events:
// 1. kernel.request - Before routing
// 2. kernel.controller - After controller resolution
// 3. kernel.view - If controller doesn't return a Response
// 4. kernel.response - Before sending the response
// 5. kernel.terminate - After sending (async tasks)
}The complete cycle: index.php → Runtime → Kernel → HttpKernel → EventDispatcher (kernel.request) → Router → Controller → EventDispatcher (kernel.response) → Response. Each step can be intercepted via Event Subscribers.
Question 2: What is the Service Container and Dependency Injection in Symfony?
The Service Container (or DIC - Dependency Injection Container) is the heart of Symfony. It manages instantiation, configuration, and injection of all application services.
// Service with automatically injected dependencies
namespace App\Service;
use App\Repository\OrderRepository;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class PaymentService
{
public function __construct(
private readonly HttpClientInterface $stripeClient, // Configured HTTP client
private readonly OrderRepository $orderRepository, // Doctrine repository
private readonly LoggerInterface $logger, // PSR-3 logger
private readonly string $stripeApiKey, // Injected parameter
) {}
public function processPayment(int $orderId, float $amount): bool
{
$order = $this->orderRepository->find($orderId);
try {
$response = $this->stripeClient->request('POST', '/charges', [
'body' => [
'amount' => $amount * 100,
'currency' => 'eur',
'source' => $order->getPaymentToken(),
],
]);
$order->markAsPaid($response->toArray()['id']);
$this->orderRepository->save($order, true);
return true;
} catch (\Exception $e) {
$this->logger->error('Payment failed', [
'order' => $orderId,
'error' => $e->getMessage(),
]);
return false;
}
}
}# config/services.yaml
# Service configuration
services:
_defaults:
autowire: true # Automatic injection by type-hint
autoconfigure: true # Automatic tag configuration
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# Explicit configuration with parameters
App\Service\PaymentService:
arguments:
$stripeClient: '@stripe.client'
$stripeApiKey: '%env(STRIPE_API_KEY)%'Autowiring automatically resolves dependencies by type-hint. Scalar parameters require explicit configuration.
Question 3: What is the Difference Between a Bundle and a Symfony Component?
Bundles are reusable packages that integrate features into a Symfony application. Components are standalone PHP libraries usable without Symfony.
// Custom Bundle structure
namespace App\MyBundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
class MyBundle extends AbstractBundle
{
// Load bundle configuration
public function loadExtension(
array $config,
ContainerConfigurator $container,
ContainerBuilder $builder
): void {
// Load bundle services
$container->import('../config/services.yaml');
// Conditional configuration
if ($config['feature_enabled']) {
$container->services()
->set('my_bundle.feature_service', FeatureService::class)
->autowire();
}
}
// Default bundle configuration
public function configure(DefinitionConfigurator $definition): void
{
$definition->rootNode()
->children()
->booleanNode('feature_enabled')->defaultTrue()->end()
->scalarNode('api_key')->isRequired()->end()
->end();
}
}// Using a Component without Symfony
// Components are standalone PHP libraries
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// Usable in any PHP project
$request = Request::createFromGlobals();
$response = new Response('Hello World', 200);
$response->send();Bundles encapsulate configuration, services, and resources. Components are low-level tools reusable anywhere.
Question 4: How Do Event Subscribers Work in Symfony?
Event Subscribers allow reacting to framework or application events, decoupling business logic from the main code.
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\KernelEvents;
class ApiExceptionSubscriber implements EventSubscriberInterface
{
// Declare listened events and their priority
public static function getSubscribedEvents(): array
{
return [
// High priority (executed before others)
KernelEvents::EXCEPTION => ['onKernelException', 100],
KernelEvents::RESPONSE => ['onKernelResponse', 0],
];
}
public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
$request = $event->getRequest();
// Only handle API requests
if (!str_starts_with($request->getPathInfo(), '/api')) {
return;
}
$statusCode = $exception instanceof HttpExceptionInterface
? $exception->getStatusCode()
: 500;
$response = new JsonResponse([
'error' => true,
'message' => $exception->getMessage(),
'code' => $statusCode,
], $statusCode);
// Replace response with our JSON
$event->setResponse($response);
}
public function onKernelResponse(\Symfony\Component\HttpKernel\Event\ResponseEvent $event): void
{
// Add custom headers
$event->getResponse()->headers->set('X-Api-Version', '1.0');
}
}Event Subscribers are auto-discovered thanks to autoconfigure. Priority determines execution order (higher = executed first).
Doctrine ORM
Question 5: Explain Doctrine Relations and Their Differences
Doctrine offers several relation types to model associations between entities. Each type has implications for queries and performance.
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
// OneToOne relation: a user has one profile
#[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])]
private ?Profile $profile = null;
// OneToMany relation: a user has multiple articles
#[ORM\OneToMany(mappedBy: 'author', targetEntity: Article::class, orphanRemoval: true)]
private Collection $articles;
// ManyToMany relation: multiple users have multiple roles
#[ORM\ManyToMany(targetEntity: Role::class, inversedBy: 'users')]
#[ORM\JoinTable(name: 'user_roles')]
private Collection $roles;
public function __construct()
{
// Mandatory collection initialization
$this->articles = new ArrayCollection();
$this->roles = new ArrayCollection();
}
public function addArticle(Article $article): static
{
if (!$this->articles->contains($article)) {
$this->articles->add($article);
$article->setAuthor($this); // Bidirectional synchronization
}
return $this;
}
public function removeArticle(Article $article): static
{
if ($this->articles->removeElement($article)) {
if ($article->getAuthor() === $this) {
$article->setAuthor(null);
}
}
return $this;
}
}#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
// ManyToOne relation: multiple articles belong to one author
#[ORM\ManyToOne(inversedBy: 'articles')]
#[ORM\JoinColumn(nullable: false)]
private ?User $author = null;
// ManyToMany with additional attributes via pivot entity
#[ORM\OneToMany(mappedBy: 'article', targetEntity: ArticleTag::class)]
private Collection $articleTags;
}Bidirectional relations require manual synchronization. The "owning" side (with JoinColumn/JoinTable) controls persistence.
Question 6: What is the N+1 Problem and How to Solve It with Doctrine?
The N+1 problem occurs when a main query generates N additional queries to load relations. This is the most common cause of slowness in Symfony applications.
namespace App\Repository;
use App\Entity\Article;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class ArticleRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Article::class);
}
// BAD: N+1 queries if accessing authors
public function findAllBad(): array
{
return $this->findAll();
// + 1 query per article to load the author
}
// GOOD: JOIN with eager fetch
public function findAllWithAuthor(): array
{
return $this->createQueryBuilder('a')
->addSelect('u') // SELECT author as well
->leftJoin('a.author', 'u') // JOIN on the relation
->orderBy('a.publishedAt', 'DESC')
->getQuery()
->getResult();
}
// GOOD: Multiple JOINs for multiple relations
public function findAllWithDetails(): array
{
return $this->createQueryBuilder('a')
->addSelect('u', 'c', 't') // SELECT all relations
->leftJoin('a.author', 'u')
->leftJoin('a.comments', 'c')
->leftJoin('a.tags', 't')
->where('a.status = :status')
->setParameter('status', 'published')
->orderBy('a.publishedAt', 'DESC')
->getQuery()
->getResult();
}
// GOOD: Batch loading for large lists
public function findAllOptimized(): array
{
$query = $this->createQueryBuilder('a')
->getQuery();
// Load relations in batches of 100
$query->setFetchMode(Article::class, 'author', ClassMetadata::FETCH_BATCH);
return $query->getResult();
}
}Use Symfony Profiler with the Doctrine panel to detect N+1 problems. The query count appears in the Web Debug Toolbar.
Question 7: How to Create Query Extensions and Doctrine Filters?
Query Extensions and Doctrine Filters allow automatically applying conditions to all queries, ideal for multi-tenancy or soft delete.
// API Platform extension to automatically filter by user
namespace App\Doctrine\Extension;
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use App\Entity\Article;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\SecurityBundle\Security;
final class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
public function __construct(
private readonly Security $security,
) {}
public function applyToCollection(
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
?Operation $operation = null,
array $context = []
): void {
$this->addWhere($queryBuilder, $resourceClass);
}
public function applyToItem(
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
array $identifiers,
?Operation $operation = null,
array $context = []
): void {
$this->addWhere($queryBuilder, $resourceClass);
}
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
{
// Only applies to Articles
if ($resourceClass !== Article::class) {
return;
}
// Admin sees everything
if ($this->security->isGranted('ROLE_ADMIN')) {
return;
}
$user = $this->security->getUser();
$rootAlias = $queryBuilder->getRootAliases()[0];
// Automatic filter by author
$queryBuilder
->andWhere(sprintf('%s.author = :current_user', $rootAlias))
->setParameter('current_user', $user);
}
}// Global Doctrine filter to exclude deleted items
namespace App\Doctrine\Filter;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
class SoftDeleteFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, string $targetTableAlias): string
{
// Check if entity has a deletedAt field
if (!$targetEntity->hasField('deletedAt')) {
return '';
}
return sprintf('%s.deleted_at IS NULL', $targetTableAlias);
}
}# config/packages/doctrine.yaml
doctrine:
orm:
filters:
soft_delete:
class: App\Doctrine\Filter\SoftDeleteFilter
enabled: trueFilters apply at the SQL level, extensions at the QueryBuilder level. Temporarily disable with $em->getFilters()->disable('soft_delete').
Ready to ace your Symfony interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Symfony Security
Question 8: How Does Symfony's Security System Work?
Symfony's Security component handles authentication (who is the user) and authorization (what can they do) via an extensible architecture.
# config/packages/security.yaml
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/api
stateless: true
jwt: ~ # Lexik JWT Bundle
main:
lazy: true
provider: app_user_provider
form_login:
login_path: app_login
check_path: app_login
enable_csrf: true
logout:
path: app_logout
remember_me:
secret: '%kernel.secret%'
lifetime: 604800
access_control:
- { path: ^/api/login, roles: PUBLIC_ACCESS }
- { path: ^/api, roles: ROLE_USER }
- { path: ^/admin, roles: ROLE_ADMIN }// Custom authenticator for specific logic
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class ApiKeyAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
// This authenticator only handles requests with X-API-KEY
return $request->headers->has('X-API-KEY');
}
public function authenticate(Request $request): Passport
{
$apiKey = $request->headers->get('X-API-KEY');
if (null === $apiKey) {
throw new AuthenticationException('No API key provided');
}
// UserBadge loads user by identifier
return new SelfValidatingPassport(
new UserBadge($apiKey, function (string $apiKey) {
// Load user by API key logic
return $this->userRepository->findByApiKey($apiKey);
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// null = continue request normally
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new JsonResponse([
'error' => $exception->getMessage(),
], Response::HTTP_UNAUTHORIZED);
}
}Security architecture relies on Firewalls (configuration), Authenticators (authentication), and Voters (authorization).
Question 9: How to Implement Voters for Fine-Grained Authorization?
Voters enable complex and reusable authorization logic, separating business rules from controller code.
namespace App\Security\Voter;
use App\Entity\Article;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class ArticleVoter extends Voter
{
public const VIEW = 'ARTICLE_VIEW';
public const EDIT = 'ARTICLE_EDIT';
public const DELETE = 'ARTICLE_DELETE';
protected function supports(string $attribute, mixed $subject): bool
{
// This voter only handles Articles and these attributes
return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE])
&& $subject instanceof Article;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
$article = $subject;
// Published articles are visible to everyone
if ($attribute === self::VIEW && $article->isPublished()) {
return true;
}
// Other actions require an authenticated user
if (!$user instanceof User) {
return false;
}
return match ($attribute) {
self::VIEW => $this->canView($article, $user),
self::EDIT => $this->canEdit($article, $user),
self::DELETE => $this->canDelete($article, $user),
default => false,
};
}
private function canView(Article $article, User $user): bool
{
// Drafts visible only to author or admins
return $article->getAuthor() === $user
|| in_array('ROLE_ADMIN', $user->getRoles());
}
private function canEdit(Article $article, User $user): bool
{
// Only author can edit
return $article->getAuthor() === $user;
}
private function canDelete(Article $article, User $user): bool
{
// Author or admin can delete
return $article->getAuthor() === $user
|| in_array('ROLE_ADMIN', $user->getRoles());
}
}// Using Voter in a controller
use Symfony\Component\Security\Http\Attribute\IsGranted;
class ArticleController extends AbstractController
{
#[Route('/articles/{id}/edit', name: 'article_edit')]
#[IsGranted(ArticleVoter::EDIT, subject: 'article')]
public function edit(Article $article): Response
{
// Authorization is checked automatically
// 403 if voter denies access
}
// Programmatic alternative
#[Route('/articles/{id}', name: 'article_show')]
public function show(Article $article): Response
{
$this->denyAccessUnlessGranted(ArticleVoter::VIEW, $article);
// Or with condition
if (!$this->isGranted(ArticleVoter::EDIT, $article)) {
// Hide edit button
}
}
}{# In Twig #}
{% if is_granted('ARTICLE_EDIT', article) %}
<a href="{{ path('article_edit', {id: article.id}) }}">Edit</a>
{% endif %}Voters are auto-discovered and automatically consulted during isGranted() calls. Default strategy grants access if at least one Voter votes positively.
Question 10: How to Secure an API with JWT in Symfony?
JWT (JSON Web Token) authentication is the standard solution for stateless APIs. Symfony typically uses the LexikJWTAuthenticationBundle.
# config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'
token_ttl: 3600 # 1 hournamespace App\Controller\Api;
use App\Entity\User;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/api')]
class AuthController extends AbstractController
{
#[Route('/login', name: 'api_login', methods: ['POST'])]
public function login(
Request $request,
UserPasswordHasherInterface $passwordHasher,
JWTTokenManagerInterface $jwtManager,
): JsonResponse {
$data = json_decode($request->getContent(), true);
$user = $this->userRepository->findOneBy(['email' => $data['email']]);
if (!$user || !$passwordHasher->isPasswordValid($user, $data['password'])) {
return new JsonResponse(['error' => 'Invalid credentials'], 401);
}
// Generate JWT token
$token = $jwtManager->create($user);
return new JsonResponse([
'token' => $token,
'user' => [
'id' => $user->getId(),
'email' => $user->getEmail(),
'roles' => $user->getRoles(),
],
]);
}
#[Route('/refresh-token', name: 'api_refresh_token', methods: ['POST'])]
public function refreshToken(): JsonResponse
{
// Handled automatically by bundle if configured
// Returns a new token from refresh token
}
}// JWT payload customization
namespace App\EventListener;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
class JWTCreatedListener
{
public function onJWTCreated(JWTCreatedEvent $event): void
{
$user = $event->getUser();
$payload = $event->getData();
// Add custom data to token
$payload['user_id'] = $user->getId();
$payload['email'] = $user->getEmail();
$payload['permissions'] = $user->getPermissions();
$event->setData($payload);
}
}The JWT token is sent in the Authorization: Bearer <token> header. The bundle automatically verifies signature and expiration.
Symfony Forms
Question 11: How to Create Advanced Forms with Validation?
Symfony's Form component generates HTML forms, handles submission, and validates data with constraints.
namespace App\Form;
use App\Entity\Article;
use App\Entity\Category;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title', TextType::class, [
'label' => 'Article Title',
'attr' => ['placeholder' => 'Enter title...'],
'constraints' => [
new Assert\NotBlank(message: 'Title is required'),
new Assert\Length(
min: 10,
max: 255,
minMessage: 'Title must be at least {{ limit }} characters',
),
],
])
->add('content', TextareaType::class, [
'label' => 'Content',
'constraints' => [
new Assert\NotBlank(),
new Assert\Length(min: 100),
],
])
->add('category', EntityType::class, [
'class' => Category::class,
'choice_label' => 'name',
'placeholder' => 'Choose a category',
'query_builder' => function ($repo) {
return $repo->createQueryBuilder('c')
->where('c.active = true')
->orderBy('c.name', 'ASC');
},
])
->add('coverImage', FileType::class, [
'label' => 'Cover Image',
'mapped' => false, // Not bound to entity
'required' => false,
'constraints' => [
new Assert\Image(
maxSize: '5M',
mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
mimeTypesMessage: 'Unsupported image format',
),
],
])
->add('publishedAt', DateTimeType::class, [
'widget' => 'single_text',
'required' => false,
'label' => 'Publication Date',
]);
// Event listener to modify form dynamically
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$article = $event->getData();
$form = $event->getForm();
// Add field only for modifications
if ($article && $article->getId()) {
$form->add('slug', TextType::class, [
'disabled' => true,
'help' => 'Slug cannot be modified',
]);
}
});
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Article::class,
'validation_groups' => ['Default', 'article_creation'],
]);
}
}#[Route('/articles/new', name: 'article_new')]
public function new(Request $request, SluggerInterface $slugger): Response
{
$article = new Article();
$form = $this->createForm(ArticleType::class, $article);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Handle file upload
$coverImage = $form->get('coverImage')->getData();
if ($coverImage) {
$filename = $slugger->slug($article->getTitle()).'-'.uniqid().'.'.$coverImage->guessExtension();
$coverImage->move($this->getParameter('covers_directory'), $filename);
$article->setCoverImageFilename($filename);
}
$this->entityManager->persist($article);
$this->entityManager->flush();
$this->addFlash('success', 'Article created successfully!');
return $this->redirectToRoute('article_show', ['id' => $article->getId()]);
}
return $this->render('article/new.html.twig', [
'form' => $form,
]);
}FormEvents (PRE_SET_DATA, POST_SUBMIT, etc.) allow dynamically modifying fields based on context.
Question 12: How to Implement Custom Validation with Constraints?
Symfony allows creating custom validation constraints for complex business rules.
// Custom constraint
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class UniqueEmail extends Constraint
{
public string $message = 'The email "{{ value }}" is already used.';
public ?int $excludeId = null; // To exclude current user in update
}// Validator associated with the constraint
namespace App\Validator;
use App\Repository\UserRepository;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class UniqueEmailValidator extends ConstraintValidator
{
public function __construct(
private readonly UserRepository $userRepository,
) {}
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof UniqueEmail) {
throw new UnexpectedTypeException($constraint, UniqueEmail::class);
}
if (null === $value || '' === $value) {
return; // Let NotBlank handle empty values
}
$existingUser = $this->userRepository->findOneBy(['email' => $value]);
// Check if user exists with this email
// and it's not the current user (for updates)
if ($existingUser && $existingUser->getId() !== $constraint->excludeId) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $value)
->addViolation();
}
}
}// Using constraint on entity
use App\Validator as AppAssert;
use Symfony\Component\Validator\Constraints as Assert;
class User
{
#[Assert\NotBlank]
#[Assert\Email]
#[AppAssert\UniqueEmail]
private ?string $email = null;
#[Assert\NotBlank]
#[Assert\Length(min: 8)]
#[Assert\Regex(
pattern: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/',
message: 'Password must contain an uppercase, lowercase, and digit'
)]
private ?string $plainPassword = null;
}// Class-level constraint for multi-field validation
// src/Validator/PasswordMatch.php
#[\Attribute(\Attribute::TARGET_CLASS)]
class PasswordMatch extends Constraint
{
public string $message = 'Passwords do not match.';
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}Custom constraints are auto-discovered. The Validator suffix is mandatory for the validator.
Messenger and Async Communication
Question 13: How to Implement Async Processing with Messenger?
Symfony Messenger dispatches messages to queues for async processing, improving application responsiveness.
// The message (DTO containing data)
namespace App\Message;
final class SendWelcomeEmail
{
public function __construct(
public readonly int $userId,
public readonly string $locale = 'en',
) {}
}// The handler that processes the message
namespace App\MessageHandler;
use App\Message\SendWelcomeEmail;
use App\Repository\UserRepository;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class SendWelcomeEmailHandler
{
public function __construct(
private readonly UserRepository $userRepository,
private readonly MailerInterface $mailer,
) {}
public function __invoke(SendWelcomeEmail $message): void
{
$user = $this->userRepository->find($message->userId);
if (!$user) {
return; // User deleted in the meantime
}
$email = (new TemplatedEmail())
->to($user->getEmail())
->subject('Welcome to our platform!')
->htmlTemplate('emails/welcome.html.twig')
->context([
'user' => $user,
'locale' => $message->locale,
]);
$this->mailer->send($email);
}
}# config/packages/messenger.yaml
framework:
messenger:
failure_transport: failed
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
failed:
dsn: 'doctrine://default?queue_name=failed'
routing:
# Route messages to async transport
App\Message\SendWelcomeEmail: async
App\Message\ProcessImage: async
App\Message\GenerateReport: async// Dispatching the message
use Symfony\Component\Messenger\MessageBusInterface;
class RegistrationController extends AbstractController
{
#[Route('/register', name: 'app_register', methods: ['POST'])]
public function register(
Request $request,
MessageBusInterface $bus,
): Response {
// ... user creation
// Async dispatch - email will be sent in background
$bus->dispatch(new SendWelcomeEmail(
userId: $user->getId(),
locale: $request->getLocale(),
));
// Immediate response to user
return $this->redirectToRoute('app_login');
}
}Start the worker with php bin/console messenger:consume async -vv. In production, use Supervisor to keep the worker running.
Question 14: How to Handle Errors and Retry with Messenger?
Messenger offers robust mechanisms for handling failures: automatic retry, dead letter queue, and manual failed message handling.
namespace App\MessageHandler;
use App\Exception\PaymentFailedException;
use App\Exception\PaymentRetryableException;
use App\Message\ProcessPayment;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
#[AsMessageHandler]
final class ProcessPaymentHandler
{
public function __invoke(ProcessPayment $message): void
{
try {
$this->paymentGateway->process($message->orderId);
} catch (PaymentRetryableException $e) {
// Temporary error (timeout, rate limit) → retry
throw new RecoverableMessageHandlingException(
$e->getMessage(),
previous: $e
);
} catch (PaymentFailedException $e) {
// Permanent error (invalid card) → no retry
throw new UnrecoverableMessageHandlingException(
$e->getMessage(),
previous: $e
);
}
}
}// Message-level retry configuration
namespace App\Message;
use Symfony\Component\Messenger\Stamp\DelayStamp;
final class ProcessPayment
{
public function __construct(
public readonly int $orderId,
public readonly int $attempt = 1,
) {}
// Custom retry delay based on attempt count
public function getRetryDelay(): int
{
return match ($this->attempt) {
1 => 5000, // 5 seconds
2 => 30000, // 30 seconds
3 => 300000, // 5 minutes
default => 600000,
};
}
}# Failed message management commands
php bin/console messenger:failed:show # List failed messages
php bin/console messenger:failed:retry # Retry all messages
php bin/console messenger:failed:retry 123 # Retry specific message
php bin/console messenger:failed:remove 123 # Remove a messageThe retry strategy and "failed" transport ensure no message is lost. Messages can be analyzed and retried manually.
Ready to ace your Symfony interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Testing in Symfony
Question 15: How to Structure Tests in Symfony?
Symfony provides PHPUnit with dedicated helpers to test different application layers: unit, functional, and integration tests.
// Unit test: tests an isolated class
namespace App\Tests\Unit\Service;
use App\Service\PriceCalculator;
use PHPUnit\Framework\TestCase;
class PriceCalculatorTest extends TestCase
{
private PriceCalculator $calculator;
protected function setUp(): void
{
$this->calculator = new PriceCalculator();
}
public function testCalculateTotalWithoutDiscount(): void
{
$total = $this->calculator->calculateTotal(100.00, 0);
$this->assertEquals(100.00, $total);
}
public function testCalculateTotalWithPercentageDiscount(): void
{
$total = $this->calculator->calculateTotal(100.00, 20);
$this->assertEquals(80.00, $total);
}
/**
* @dataProvider discountProvider
*/
public function testCalculateTotalWithVariousDiscounts(
float $price,
int $discount,
float $expected
): void {
$total = $this->calculator->calculateTotal($price, $discount);
$this->assertEquals($expected, $total);
}
public static function discountProvider(): array
{
return [
'no discount' => [100.00, 0, 100.00],
'10% discount' => [100.00, 10, 90.00],
'50% discount' => [200.00, 50, 100.00],
'max discount' => [100.00, 100, 0.00],
];
}
}// Functional test: tests controllers via HTTP
namespace App\Tests\Functional\Controller;
use App\Entity\Article;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ArticleControllerTest extends WebTestCase
{
private $client;
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
$this->client = static::createClient();
$this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
}
public function testListArticlesReturnsOk(): void
{
$this->client->request('GET', '/articles');
$this->assertResponseIsSuccessful();
$this->assertSelectorExists('h1');
}
public function testCreateArticleRequiresAuthentication(): void
{
$this->client->request('GET', '/articles/new');
$this->assertResponseRedirects('/login');
}
public function testAuthenticatedUserCanCreateArticle(): void
{
// Authentication
$user = $this->createUser();
$this->client->loginUser($user);
// Access form
$crawler = $this->client->request('GET', '/articles/new');
$this->assertResponseIsSuccessful();
// Submit form
$form = $crawler->selectButton('Create')->form([
'article[title]' => 'Test Article Title',
'article[content]' => 'This is the content of my test article with enough characters.',
]);
$this->client->submit($form);
// Verification
$this->assertResponseRedirects();
$this->client->followRedirect();
$this->assertSelectorTextContains('h1', 'Test Article Title');
// Database verification
$article = $this->entityManager->getRepository(Article::class)
->findOneBy(['title' => 'Test Article Title']);
$this->assertNotNull($article);
}
private function createUser(): User
{
$user = new User();
$user->setEmail('test@example.com');
$user->setPassword('$2y$13$hashedpassword');
$this->entityManager->persist($user);
$this->entityManager->flush();
return $user;
}
protected function tearDown(): void
{
// Clean up test database
$this->entityManager->getConnection()->executeStatement('DELETE FROM article');
$this->entityManager->getConnection()->executeStatement('DELETE FROM user');
parent::tearDown();
}
}Separate Unit tests (no kernel), Functional tests (with kernel), and Integration tests (real services).
Question 16: How to Use Fixtures and DatabaseResetter?
Fixtures populate the database with realistic test data. The DoctrineTestBundle component facilitates reset between tests.
namespace App\DataFixtures;
use App\Entity\Article;
use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
class ArticleFixtures extends Fixture implements DependentFixtureInterface
{
public function load(ObjectManager $manager): void
{
for ($i = 1; $i <= 20; $i++) {
$article = new Article();
$article->setTitle("Test Article Number $i");
$article->setSlug("test-article-$i");
$article->setContent("Detailed content of article number $i...");
$article->setStatus($i <= 15 ? 'published' : 'draft');
$article->setPublishedAt($i <= 15 ? new \DateTimeImmutable("-$i days") : null);
// Reference to user created by UserFixtures
$article->setAuthor($this->getReference('user-'.($i % 3), User::class));
$manager->persist($article);
// Create references for other fixtures
$this->addReference("article-$i", $article);
}
$manager->flush();
}
public function getDependencies(): array
{
// UserFixtures must be loaded before ArticleFixtures
return [
UserFixtures::class,
];
}
}namespace App\DataFixtures;
use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class UserFixtures extends Fixture implements FixtureGroupInterface
{
public function __construct(
private readonly UserPasswordHasherInterface $passwordHasher,
) {}
public function load(ObjectManager $manager): void
{
$users = [
['email' => 'admin@example.com', 'roles' => ['ROLE_ADMIN'], 'ref' => 'user-0'],
['email' => 'author@example.com', 'roles' => ['ROLE_AUTHOR'], 'ref' => 'user-1'],
['email' => 'user@example.com', 'roles' => ['ROLE_USER'], 'ref' => 'user-2'],
];
foreach ($users as $userData) {
$user = new User();
$user->setEmail($userData['email']);
$user->setRoles($userData['roles']);
$user->setPassword($this->passwordHasher->hashPassword($user, 'password123'));
$manager->persist($user);
$this->addReference($userData['ref'], $user);
}
$manager->flush();
}
public static function getGroups(): array
{
return ['test', 'dev'];
}
}// Using DAMADoctrineTestBundle for automatic reset
namespace App\Tests\Functional;
use App\Entity\Article;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ArticleControllerTest extends WebTestCase
{
use \DAMA\DoctrineTestBundle\Doctrine\DBAL\StaticDriver;
public function testPublishedArticlesCount(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$count = $em->getRepository(Article::class)
->count(['status' => 'published']);
$this->assertEquals(15, $count); // According to fixtures
}
}# Loading fixtures
php bin/console doctrine:fixtures:load
php bin/console doctrine:fixtures:load --group=test
php bin/console doctrine:fixtures:load --env=testDAMADoctrineTestBundle wraps each test in a transaction that is rolled back, avoiding fixture reloading between tests.
Architecture and Advanced Patterns
Question 17: How to Implement CQRS with Symfony?
CQRS (Command Query Responsibility Segregation) separates read operations from write operations, allowing independent optimization.
// Command: represents an intent to modify
namespace App\Message\Command;
final class CreateArticleCommand
{
public function __construct(
public readonly string $title,
public readonly string $content,
public readonly int $authorId,
public readonly array $tagIds = [],
) {}
}// CommandHandler: executes the modification
namespace App\MessageHandler\Command;
use App\Entity\Article;
use App\Message\Command\CreateArticleCommand;
use App\Repository\TagRepository;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\String\Slugger\SluggerInterface;
#[AsMessageHandler]
final class CreateArticleCommandHandler
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly UserRepository $userRepository,
private readonly TagRepository $tagRepository,
private readonly SluggerInterface $slugger,
) {}
public function __invoke(CreateArticleCommand $command): Article
{
$author = $this->userRepository->find($command->authorId)
?? throw new \InvalidArgumentException('Author not found');
$article = new Article();
$article->setTitle($command->title);
$article->setSlug($this->slugger->slug($command->title)->lower());
$article->setContent($command->content);
$article->setAuthor($author);
foreach ($command->tagIds as $tagId) {
$tag = $this->tagRepository->find($tagId);
if ($tag) {
$article->addTag($tag);
}
}
$this->entityManager->persist($article);
$this->entityManager->flush();
return $article;
}
}// Query: represents a read request
namespace App\Message\Query;
final class GetArticleBySlugQuery
{
public function __construct(
public readonly string $slug,
public readonly bool $withComments = false,
) {}
}// QueryHandler: retrieves data (can use optimized read model)
namespace App\MessageHandler\Query;
use App\DTO\ArticleDTO;
use App\Message\Query\GetArticleBySlugQuery;
use App\Repository\ArticleRepository;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class GetArticleBySlugQueryHandler
{
public function __construct(
private readonly ArticleRepository $repository,
) {}
public function __invoke(GetArticleBySlugQuery $query): ?ArticleDTO
{
$qb = $this->repository->createQueryBuilder('a')
->select('a', 'u')
->leftJoin('a.author', 'u')
->where('a.slug = :slug')
->setParameter('slug', $query->slug);
if ($query->withComments) {
$qb->addSelect('c')
->leftJoin('a.comments', 'c');
}
$article = $qb->getQuery()->getOneOrNullResult();
return $article ? ArticleDTO::fromEntity($article) : null;
}
}// Using Command and Query Buses
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\HandledStamp;
class ArticleController extends AbstractController
{
public function __construct(
private readonly MessageBusInterface $commandBus,
private readonly MessageBusInterface $queryBus,
) {}
#[Route('/articles', methods: ['POST'])]
public function create(Request $request): Response
{
$data = json_decode($request->getContent(), true);
$envelope = $this->commandBus->dispatch(new CreateArticleCommand(
title: $data['title'],
content: $data['content'],
authorId: $this->getUser()->getId(),
));
$article = $envelope->last(HandledStamp::class)->getResult();
return $this->json($article, 201);
}
#[Route('/articles/{slug}', methods: ['GET'])]
public function show(string $slug): Response
{
$envelope = $this->queryBus->dispatch(new GetArticleBySlugQuery(
slug: $slug,
withComments: true,
));
$article = $envelope->last(HandledStamp::class)->getResult();
return $this->json($article);
}
}CQRS allows optimizing reads (caching, projections) and writes (validation, events) separately.
Question 18: How to Implement the Repository Pattern Correctly in Symfony?
The Repository Pattern in Symfony is already present via Doctrine but can be enriched with interfaces and business methods.
// Interface for decoupling and testability
namespace App\Repository\Contract;
use App\Entity\Article;
use Doctrine\Common\Collections\Collection;
interface ArticleRepositoryInterface
{
public function find(int $id): ?Article;
public function findBySlug(string $slug): ?Article;
public function findPublished(int $limit = 20, int $offset = 0): array;
public function findByAuthor(int $authorId): array;
public function save(Article $article, bool $flush = false): void;
public function remove(Article $article, bool $flush = false): void;
}// Doctrine implementation of the repository
namespace App\Repository;
use App\Entity\Article;
use App\Repository\Contract\ArticleRepositoryInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class ArticleRepository extends ServiceEntityRepository implements ArticleRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Article::class);
}
public function findBySlug(string $slug): ?Article
{
return $this->createQueryBuilder('a')
->addSelect('u', 't')
->leftJoin('a.author', 'u')
->leftJoin('a.tags', 't')
->where('a.slug = :slug')
->setParameter('slug', $slug)
->getQuery()
->getOneOrNullResult();
}
public function findPublished(int $limit = 20, int $offset = 0): array
{
return $this->createQueryBuilder('a')
->addSelect('u')
->leftJoin('a.author', 'u')
->where('a.status = :status')
->setParameter('status', 'published')
->orderBy('a.publishedAt', 'DESC')
->setMaxResults($limit)
->setFirstResult($offset)
->getQuery()
->getResult();
}
public function findByAuthor(int $authorId): array
{
return $this->createQueryBuilder('a')
->where('a.author = :authorId')
->setParameter('authorId', $authorId)
->orderBy('a.createdAt', 'DESC')
->getQuery()
->getResult();
}
public function save(Article $article, bool $flush = false): void
{
$this->getEntityManager()->persist($article);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(Article $article, bool $flush = false): void
{
$this->getEntityManager()->remove($article);
if ($flush) {
$this->getEntityManager()->flush();
}
}
// Complex query methods
public function findPopularByCategory(int $categoryId, int $limit = 5): array
{
return $this->createQueryBuilder('a')
->addSelect('u')
->leftJoin('a.author', 'u')
->where('a.category = :categoryId')
->andWhere('a.status = :status')
->setParameter('categoryId', $categoryId)
->setParameter('status', 'published')
->orderBy('a.viewCount', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
}# config/services.yaml
# Binding interface to implementation
services:
App\Repository\Contract\ArticleRepositoryInterface:
alias: App\Repository\ArticleRepositoryThe interface allows creating test implementations (InMemoryArticleRepository) or changing data source without modifying business code.
Question 19: How to Manage Configuration and Environments in Symfony?
Symfony uses a flexible configuration system with support for environment variables, secrets, and YAML files per environment.
# config/packages/framework.yaml
# Default configuration
framework:
secret: '%env(APP_SECRET)%'
http_method_override: false
handle_all_throwables: true
session:
handler_id: null
cookie_secure: auto
cookie_samesite: lax
storage_factory_id: session.storage.factory.native
php_errors:
log: true# config/packages/prod/framework.yaml
# Override for production
framework:
session:
handler_id: '%env(REDIS_URL)%'
cookie_secure: true
when@prod:
framework:
router:
strict_requirements: null// Managing sensitive secrets (encrypted)
// php bin/console secrets:set DATABASE_URL --env=prod
// php bin/console secrets:list --reveal --env=prod// Custom configuration for a bundle
namespace App\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('my_app');
$treeBuilder->getRootNode()
->children()
->scalarNode('api_key')
->isRequired()
->cannotBeEmpty()
->end()
->integerNode('cache_ttl')
->defaultValue(3600)
->min(0)
->end()
->arrayNode('features')
->addDefaultsIfNotSet()
->children()
->booleanNode('dark_mode')->defaultTrue()->end()
->booleanNode('beta_features')->defaultFalse()->end()
->end()
->end()
->end();
return $treeBuilder;
}
}// Configuration injection
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class ConfigurableService
{
public function __construct(
#[Autowire('%env(API_KEY)%')]
private readonly string $apiKey,
#[Autowire('%kernel.project_dir%')]
private readonly string $projectDir,
#[Autowire('%my_app.cache_ttl%')]
private readonly int $cacheTtl,
) {}
}Symfony secrets are encrypted and versioned. Use %env(...)% for runtime variables, parameters for static values.
Performance and Production
Question 20: How to Optimize Symfony Application Performance?
Optimization covers several levels: opcache, configuration, application cache, and Doctrine queries.
# Optimized Doctrine configuration for production
doctrine:
orm:
auto_generate_proxy_classes: false
metadata_cache_driver:
type: pool
pool: doctrine.system_cache_pool
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool# config/packages/cache.yaml
# Cache configuration with Redis
framework:
cache:
app: cache.adapter.redis
system: cache.adapter.system
pools:
doctrine.result_cache_pool:
adapter: cache.adapter.redis
default_lifetime: 3600
doctrine.system_cache_pool:
adapter: cache.adapter.system
services:
Redis:
class: Redis
calls:
- connect:
- '%env(REDIS_HOST)%'
- '%env(int:REDIS_PORT)%'
Symfony\Component\Cache\Adapter\RedisAdapter:
arguments:
- '@Redis'// Using Doctrine result cache
public function findPopularCached(): array
{
return $this->createQueryBuilder('a')
->addSelect('u')
->leftJoin('a.author', 'u')
->where('a.status = :status')
->setParameter('status', 'published')
->orderBy('a.viewCount', 'DESC')
->setMaxResults(10)
->getQuery()
->enableResultCache(3600, 'popular_articles') // Cache 1h
->getResult();
}# Production optimization commands
php bin/console cache:clear --env=prod
php bin/console cache:warmup --env=prod
php bin/console doctrine:cache:clear-metadata --env=prod
php bin/console doctrine:cache:clear-query --env=prod
# Generate Doctrine proxies
php bin/console doctrine:proxy:create-proxy-classes
# Optimized autoloader compilation
composer install --no-dev --optimize-autoloader --classmap-authoritativeOPcache must be enabled in production with optimal settings. Warmup generates the container and router cache.
Question 21: How to Configure Logging and Monitoring in Symfony?
Structured logging and proper monitoring are essential for diagnosing production issues.
# config/packages/prod/monolog.yaml
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50
nested:
type: stream
path: '%kernel.logs_dir%/%kernel.environment%.log'
level: debug
formatter: monolog.formatter.json
console:
type: console
process_psr_3_messages: false
channels: ['!event', '!doctrine']
slack:
type: slack
token: '%env(SLACK_TOKEN)%'
channel: '#alerts'
level: critical
bot_name: 'SymfonyBot'// Structured logging with context
namespace App\Service;
use Psr\Log\LoggerInterface;
class PaymentService
{
public function __construct(
private readonly LoggerInterface $logger,
) {}
public function processPayment(Order $order): bool
{
$this->logger->info('Payment processing started', [
'order_id' => $order->getId(),
'amount' => $order->getTotal(),
'currency' => $order->getCurrency(),
'user_id' => $order->getUser()->getId(),
]);
try {
$result = $this->gateway->charge($order);
$this->logger->info('Payment successful', [
'order_id' => $order->getId(),
'transaction_id' => $result->getTransactionId(),
]);
return true;
} catch (\Exception $e) {
$this->logger->error('Payment failed', [
'order_id' => $order->getId(),
'error' => $e->getMessage(),
'exception' => $e,
]);
return false;
}
}
}// HTTP request logging
namespace App\EventSubscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class RequestLoggerSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly LoggerInterface $logger,
) {}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::TERMINATE => 'onKernelTerminate',
];
}
public function onKernelTerminate(TerminateEvent $event): void
{
$request = $event->getRequest();
$response = $event->getResponse();
$this->logger->info('Request completed', [
'method' => $request->getMethod(),
'uri' => $request->getRequestUri(),
'status' => $response->getStatusCode(),
'duration_ms' => round((microtime(true) - $request->server->get('REQUEST_TIME_FLOAT')) * 1000),
'memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
}JSON format facilitates ingestion by monitoring tools (ELK, Datadog). Channels allow filtering by log type.
Question 22: How to Deploy a Symfony Application to Production?
A robust Symfony deployment combines build preparation, safe migrations, and atomic switching.
#!/bin/bash
# deploy.sh - Deployment script
set -e
RELEASE_DIR="/var/www/releases/$(date +%Y%m%d%H%M%S)"
SHARED_DIR="/var/www/shared"
CURRENT_LINK="/var/www/current"
echo "Creating release directory..."
mkdir -p $RELEASE_DIR
echo "Cloning repository..."
git clone --depth 1 --branch main git@github.com:org/repo.git $RELEASE_DIR
echo "Installing dependencies..."
cd $RELEASE_DIR
composer install --no-dev --optimize-autoloader --classmap-authoritative
echo "Linking shared files..."
ln -sf $SHARED_DIR/.env.local $RELEASE_DIR/.env.local
ln -sf $SHARED_DIR/var/log $RELEASE_DIR/var/log
ln -sf $SHARED_DIR/public/uploads $RELEASE_DIR/public/uploads
echo "Running migrations..."
php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
echo "Warming up cache..."
php bin/console cache:clear --env=prod --no-debug
php bin/console cache:warmup --env=prod --no-debug
echo "Setting permissions..."
chown -R www-data:www-data $RELEASE_DIR
echo "Switching to new release..."
ln -sfn $RELEASE_DIR $CURRENT_LINK
echo "Restarting PHP-FPM..."
sudo systemctl reload php8.3-fpm
echo "Restarting Messenger workers..."
php bin/console messenger:stop-workers
echo "Cleaning old releases..."
ls -dt /var/www/releases/*/ | tail -n +6 | xargs rm -rf
echo "Deployment complete!"# .github/workflows/deploy.yml
# CI/CD deployment with GitHub Actions
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: intl, pdo_pgsql, redis
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Run tests
run: php bin/phpunit
- name: Deploy to production
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
cd /var/www/app
./deploy.shAtomic deployment via symlinks allows instant rollback. Messenger workers must be restarted to load new code.
API Platform and REST
Question 23: How to Build a REST API with API Platform?
API Platform is the standard solution for creating REST and GraphQL APIs with Symfony, offering auto-documentation and HTTP standards.
// API Platform resource configuration
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
#[ApiResource(
operations: [
new GetCollection(
normalizationContext: ['groups' => ['article:list']],
),
new Get(
normalizationContext: ['groups' => ['article:read']],
),
new Post(
security: "is_granted('ROLE_AUTHOR')",
denormalizationContext: ['groups' => ['article:write']],
),
new Put(
security: "is_granted('ARTICLE_EDIT', object)",
),
new Patch(
security: "is_granted('ARTICLE_EDIT', object)",
),
new Delete(
security: "is_granted('ARTICLE_DELETE', object)",
),
],
order: ['publishedAt' => 'DESC'],
paginationItemsPerPage: 20,
)]
class Article
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['article:list', 'article:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
#[Assert\Length(min: 10, max: 255)]
#[Groups(['article:list', 'article:read', 'article:write'])]
private ?string $title = null;
#[ORM\Column(type: 'text')]
#[Assert\NotBlank]
#[Groups(['article:read', 'article:write'])]
private ?string $content = null;
#[ORM\ManyToOne(inversedBy: 'articles')]
#[ORM\JoinColumn(nullable: false)]
#[Groups(['article:list', 'article:read'])]
private ?User $author = null;
#[ORM\Column(nullable: true)]
#[Groups(['article:list', 'article:read'])]
private ?\DateTimeImmutable $publishedAt = null;
}// Custom processor for business logic
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Article;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\String\Slugger\SluggerInterface;
final class ArticleProcessor implements ProcessorInterface
{
public function __construct(
private readonly ProcessorInterface $persistProcessor,
private readonly Security $security,
private readonly SluggerInterface $slugger,
) {}
public function process(
mixed $data,
Operation $operation,
array $uriVariables = [],
array $context = []
): Article {
if ($data instanceof Article && !$data->getId()) {
// New article: set author and slug
$data->setAuthor($this->security->getUser());
$data->setSlug($this->slugger->slug($data->getTitle())->lower());
}
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
}
}# config/packages/api_platform.yaml
api_platform:
title: 'My API'
version: '1.0.0'
formats:
jsonld: ['application/ld+json']
json: ['application/json']
docs_formats:
jsonld: ['application/ld+json']
jsonopenapi: ['application/vnd.openapi+json']
html: ['text/html']
defaults:
pagination_items_per_page: 20
pagination_client_items_per_page: true
pagination_maximum_items_per_page: 100
swagger:
versions: [3]API Platform automatically generates OpenAPI documentation and provides filters, pagination, and validation.
Question 24: How to Customize API Platform Operations?
API Platform allows creating custom operations with controllers or State Providers/Processors.
// Custom operations
#[ApiResource(
operations: [
// Standard operations
new GetCollection(),
new Get(),
// Custom operation with controller
new Post(
uriTemplate: '/articles/{id}/publish',
controller: PublishArticleController::class,
openapi: new Model\Operation(
summary: 'Publishes an article',
description: 'Changes article status to "published"',
),
security: "is_granted('ARTICLE_EDIT', object)",
),
// Operation with custom State Provider
new GetCollection(
uriTemplate: '/articles/trending',
provider: TrendingArticlesProvider::class,
openapiContext: ['summary' => 'Trending articles'],
),
],
)]
class Article
{
// ...
}// Controller for custom operation
namespace App\Controller;
use App\Entity\Article;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\AsController;
#[AsController]
class PublishArticleController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
) {}
public function __invoke(Article $article): Article
{
if ($article->getStatus() === 'published') {
throw $this->createNotFoundException('Article already published');
}
$article->setStatus('published');
$article->setPublishedAt(new \DateTimeImmutable());
$this->entityManager->flush();
return $article;
}
}// State Provider for custom read logic
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Repository\ArticleRepository;
final class TrendingArticlesProvider implements ProviderInterface
{
public function __construct(
private readonly ArticleRepository $repository,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
{
// Custom logic for trending articles
return $this->repository->createQueryBuilder('a')
->where('a.status = :status')
->andWhere('a.publishedAt > :date')
->setParameter('status', 'published')
->setParameter('date', new \DateTimeImmutable('-7 days'))
->orderBy('a.viewCount', 'DESC')
->setMaxResults(10)
->getQuery()
->getResult();
}
}State Providers handle reading, State Processors handle writing. Controllers remain available for complex cases.
Question 25: How to Handle Database Migrations in Production?
Doctrine migrations must be designed to run without downtime and allow easy rollback.
// Safe migration: adding nullable column
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260202100000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add role column to users table (nullable first)';
}
public function up(Schema $schema): void
{
// Step 1: Add nullable column
$this->addSql('ALTER TABLE users ADD role VARCHAR(50) DEFAULT NULL');
// Create index CONCURRENTLY (PostgreSQL - non-blocking)
$this->addSql('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_role ON users (role)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX IF EXISTS idx_users_role');
$this->addSql('ALTER TABLE users DROP COLUMN role');
}
}// Data migration (separate)
final class Version20260202100001 extends AbstractMigration
{
public function getDescription(): string
{
return 'Populate role column with default value';
}
public function up(Schema $schema): void
{
// Batch migration for large tables
$this->addSql("UPDATE users SET role = 'ROLE_USER' WHERE role IS NULL");
}
public function down(Schema $schema): void
{
// No rollback needed for data
}
}// Final migration: make column NOT NULL
final class Version20260202100002 extends AbstractMigration
{
public function getDescription(): string
{
return 'Make role column NOT NULL';
}
public function up(Schema $schema): void
{
// Pre-verification
$count = $this->connection->fetchOne('SELECT COUNT(*) FROM users WHERE role IS NULL');
if ($count > 0) {
throw new \RuntimeException("$count users still have NULL role");
}
$this->addSql('ALTER TABLE users ALTER COLUMN role SET NOT NULL');
$this->addSql("ALTER TABLE users ALTER COLUMN role SET DEFAULT 'ROLE_USER'");
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE users ALTER COLUMN role DROP NOT NULL');
$this->addSql('ALTER TABLE users ALTER COLUMN role DROP DEFAULT');
}
}# Migration management commands
php bin/console doctrine:migrations:status # Migration status
php bin/console doctrine:migrations:migrate # Execute migrations
php bin/console doctrine:migrations:migrate prev # Rollback last migration
php bin/console doctrine:migrations:diff # Generate migration from schema
php bin/console doctrine:migrations:execute --down # Specific rollbackThe expand-contract strategy (3 deployments) ensures zero-downtime deployment: add nullable → migrate data → add constraint.
Conclusion
These 25 questions cover the essentials of Symfony interviews, from Service Container fundamentals to advanced production patterns.
Preparation checklist:
- ✅ Service Container and dependency injection
- ✅ Doctrine ORM: relations, queries, filters
- ✅ Security: authentication, voters, JWT
- ✅ Forms and custom validation
- ✅ Messenger: async processing and error handling
- ✅ Testing: unit, functional, fixtures
- ✅ Architecture: CQRS, Repository Pattern
- ✅ API Platform: REST, custom operations
- ✅ Production: performance, logging, deployment
Each question deserves deeper exploration with Symfony's official documentation. Recruiters value candidates who understand the framework's architectural choices and can justify their technical decisions.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

Doctrine ORM: Mastering Relationships in Symfony
Complete guide to Doctrine ORM relationships in Symfony. OneToMany, ManyToMany, loading strategies, and performance optimization with practical examples.

Symfony 7: API Platform and Best Practices
Complete guide to building professional REST APIs with Symfony 7 and API Platform 4. State Providers, Processors, validation, and serialization explained.

SQL for Data Analysts: Window Functions, CTEs and Advanced Queries
Master SQL window functions (ROW_NUMBER, RANK, LAG/LEAD), Common Table Expressions, and advanced query techniques essential for data analyst interviews and daily work.