# 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. - Published: 2026-02-02 - Updated: 2026-03-31 - Author: SharpSkill - Tags: symfony, php, interview, doctrine, technical interview - Reading time: 24 min --- 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. > **Interview Tip** > > 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. ```php // public/index.php // 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']); }; ``` ```php // src/Kernel.php // 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. ```php // src/Service/PaymentService.php // 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; } } } ``` ```yaml # 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. ```php // src/MyBundle/MyBundle.php // 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(); } } ``` ```php // 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. ```php // src/EventSubscriber/ApiExceptionSubscriber.php 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. ```php // src/Entity/User.php 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; } } ``` ```php // src/Entity/Article.php #[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. ```php // src/Repository/ArticleRepository.php 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. ```php // src/Doctrine/Extension/CurrentUserExtension.php // 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); } } ``` ```php // src/Doctrine/Filter/SoftDeleteFilter.php // 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); } } ``` ```yaml # config/packages/doctrine.yaml doctrine: orm: filters: soft_delete: class: App\Doctrine\Filter\SoftDeleteFilter enabled: true ``` Filters apply at the SQL level, extensions at the QueryBuilder level. Temporarily disable with `$em->getFilters()->disable('soft_delete')`. ## 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. ```yaml # 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 } ``` ```php // src/Security/CustomAuthenticator.php // 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. ```php // src/Security/Voter/ArticleVoter.php 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()); } } ``` ```php // src/Controller/ArticleController.php // 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 } } } ``` ```twig {# In Twig #} {% if is_granted('ARTICLE_EDIT', article) %} Edit {% 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. ```yaml # 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 hour ``` ```php // src/Controller/Api/AuthController.php namespace 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 } } ``` ```php // src/EventListener/JWTCreatedListener.php // 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 ` 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. ```php // src/Form/ArticleType.php 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'], ]); } } ``` ```php // src/Controller/ArticleController.php #[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. ```php // src/Validator/UniqueEmail.php // 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 } ``` ```php // src/Validator/UniqueEmailValidator.php // 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(); } } } ``` ```php // src/Entity/User.php // 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; } ``` ```php // 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. ```php // src/Message/SendWelcomeEmail.php // The message (DTO containing data) namespace App\Message; final class SendWelcomeEmail { public function __construct( public readonly int $userId, public readonly string $locale = 'en', ) {} } ``` ```php // src/MessageHandler/SendWelcomeEmailHandler.php // 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); } } ``` ```yaml # 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 ``` ```php // src/Controller/RegistrationController.php // 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. ```php // src/MessageHandler/ProcessPaymentHandler.php 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 ); } } } ``` ```php // src/Message/ProcessPayment.php // 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, }; } } ``` ```bash # 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 message ``` The retry strategy and "failed" transport ensure no message is lost. Messages can be analyzed and retried manually. ## 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. ```php // tests/Unit/Service/PriceCalculatorTest.php // 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], ]; } } ``` ```php // tests/Functional/Controller/ArticleControllerTest.php // 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. ```php // src/DataFixtures/ArticleFixtures.php 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, ]; } } ``` ```php // src/DataFixtures/UserFixtures.php 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']; } } ``` ```php // tests/Functional/ArticleControllerTest.php // 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 } } ``` ```bash # Loading fixtures php bin/console doctrine:fixtures:load php bin/console doctrine:fixtures:load --group=test php bin/console doctrine:fixtures:load --env=test ``` DAMADoctrineTestBundle 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. ```php // src/Message/Command/CreateArticleCommand.php // 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 = [], ) {} } ``` ```php // src/MessageHandler/Command/CreateArticleCommandHandler.php // 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; } } ``` ```php // src/Message/Query/GetArticleBySlugQuery.php // Query: represents a read request namespace App\Message\Query; final class GetArticleBySlugQuery { public function __construct( public readonly string $slug, public readonly bool $withComments = false, ) {} } ``` ```php // src/MessageHandler/Query/GetArticleBySlugQueryHandler.php // 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; } } ``` ```php // src/Controller/ArticleController.php // 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. ```php // src/Repository/Contract/ArticleRepositoryInterface.php // 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; } ``` ```php // src/Repository/ArticleRepository.php // 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(); } } ``` ```yaml # config/services.yaml # Binding interface to implementation services: App\Repository\Contract\ArticleRepositoryInterface: alias: App\Repository\ArticleRepository ``` The 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. ```yaml # 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 ``` ```yaml # 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 ``` ```php // config/secrets/prod/prod.decrypt.private.php // Managing sensitive secrets (encrypted) // php bin/console secrets:set DATABASE_URL --env=prod // php bin/console secrets:list --reveal --env=prod ``` ```php // src/DependencyInjection/Configuration.php // 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; } } ``` ```php // src/Service/ConfigurableService.php // 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. ```php // config/packages/prod/doctrine.yaml # 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 ``` ```yaml # 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' ``` ```php // src/Repository/ArticleRepository.php // 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(); } ``` ```bash # 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-authoritative ``` OPcache 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. ```yaml # 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' ``` ```php // src/Service/PaymentService.php // 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; } } } ``` ```php // src/EventSubscriber/RequestLoggerSubscriber.php // 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. ```bash #!/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!" ``` ```yaml # .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.sh ``` Atomic 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. ```php // src/Entity/Article.php // 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; } ``` ```php // src/State/ArticleProcessor.php // 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); } } ``` ```yaml # 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. ```php // src/Entity/Article.php // 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 { // ... } ``` ```php // src/Controller/PublishArticleController.php // 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; } } ``` ```php // src/State/TrendingArticlesProvider.php // 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. ```php // migrations/Version20260202100000.php // 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'); } } ``` ```php // migrations/Version20260202100001.php // 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 } } ``` ```php // migrations/Version20260202100002.php // 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'); } } ``` ```bash # 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 rollback ``` The 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 > **Going Further** > > 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/symfony/symfony-interview-questions