# Symfony-sollicitatievragen: Top 25 in 2026 > De 25 meest gestelde Symfony-sollicitatievragen. Architectuur, Doctrine ORM, services, beveiliging, formulieren en tests met gedetailleerde antwoorden en codevoorbeelden. - Published: 2026-02-02 - Updated: 2026-04-27 - Author: SharpSkill - Tags: symfony, php, interview, doctrine, technical interview - Reading time: 24 min --- Symfony-sollicitatiegesprekken toetsen de beheersing van het toonaangevende professionele PHP-framework, het begrip van componentgerichte architectuur, de Doctrine ORM en de capaciteit om robuuste en schaalbare applicaties te bouwen. Deze gids behandelt de 25 meest gestelde vragen, van Symfony-fundamenten tot geavanceerde productiepatronen. > **Tip voor het gesprek** > > Recruiters waarderen kandidaten die de Symfony-filosofie begrijpen: ontkoppeling via services, expliciete configuratie en naleving van PSR-standaarden. De architectonische keuzes van het framework kunnen uitleggen, maakt het verschil. ## Symfony-fundamenten ### Vraag 1: Leg de levenscyclus van een request uit in Symfony De levenscyclus van een Symfony-request doorloopt de HTTP Kernel en gebruikt een eventsysteem om uitbreiding bij elke stap mogelijk te maken. Dit begrip is essentieel voor debugging en het aanpassen van applicatiegedrag. ```php // public/index.php // Ingangspunt voor alle HTTP-requests use App\Kernel; require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; // Symfony Runtime regelt het bootstrappen return function (array $context) { return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); }; ``` ```php // src/Kernel.php // De Kernel orkestreert de verwerking van het request namespace App; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\HttpKernel\Kernel as BaseKernel; class Kernel extends BaseKernel { use MicroKernelTrait; // De Kernel laadt bundles en configureert de container // Belangrijke events in de levenscyclus: // 1. kernel.request - Vóór de routing // 2. kernel.controller - Na controller-resolutie // 3. kernel.view - Als de controller geen Response retourneert // 4. kernel.response - Vóór het verzenden van het antwoord // 5. kernel.terminate - Na verzending (asynchrone taken) } ``` De volledige cyclus: `index.php` → Runtime → Kernel → HttpKernel → EventDispatcher (kernel.request) → Router → Controller → EventDispatcher (kernel.response) → Response. Elke stap kan worden onderschept via Event Subscribers. ### Vraag 2: Wat zijn de Service Container en Dependency Injection in Symfony? De Service Container (of DIC, Dependency Injection Container) is het hart van Symfony. Hij beheert instantiatie, configuratie en injectie van alle services in de applicatie. ```php // src/Service/PaymentService.php // Service met automatisch geïnjecteerde 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, // Geconfigureerde HTTP-client private readonly OrderRepository $orderRepository, // Doctrine-repository private readonly LoggerInterface $logger, // PSR-3-logger private readonly string $stripeApiKey, // Geïnjecteerde 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-configuratie services: _defaults: autowire: true # Automatische injectie via type-hint autoconfigure: true # Automatische tag-configuratie App\: resource: '../src/' exclude: - '../src/DependencyInjection/' - '../src/Entity/' - '../src/Kernel.php' # Expliciete configuratie met parameters App\Service\PaymentService: arguments: $stripeClient: '@stripe.client' $stripeApiKey: '%env(STRIPE_API_KEY)%' ``` Autowiring lost dependencies automatisch op via type-hint. Scalaire parameters vereisen expliciete configuratie. ### Vraag 3: Wat is het verschil tussen een Bundle en een Symfony-component? Bundles zijn herbruikbare pakketten die functionaliteit integreren in een Symfony-applicatie. Componenten zijn op zichzelf staande PHP-bibliotheken die zonder Symfony bruikbaar zijn. ```php // src/MyBundle/MyBundle.php // Structuur van een aangepaste Bundle 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 { // Laadt de bundle-configuratie public function loadExtension( array $config, ContainerConfigurator $container, ContainerBuilder $builder ): void { // Laadt de bundle-services $container->import('../config/services.yaml'); // Voorwaardelijke configuratie if ($config['feature_enabled']) { $container->services() ->set('my_bundle.feature_service', FeatureService::class) ->autowire(); } } // Standaardconfiguratie van de bundle public function configure(DefinitionConfigurator $definition): void { $definition->rootNode() ->children() ->booleanNode('feature_enabled')->defaultTrue()->end() ->scalarNode('api_key')->isRequired()->end() ->end(); } } ``` ```php // Een component gebruiken zonder Symfony // Componenten zijn op zichzelf staande PHP-bibliotheken use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; // Bruikbaar in elk PHP-project $request = Request::createFromGlobals(); $response = new Response('Hello World', 200); $response->send(); ``` Bundles bundelen configuratie, services en resources. Componenten zijn low-level tools die overal hergebruikt kunnen worden. ### Vraag 4: Hoe werken Event Subscribers in Symfony? Event Subscribers maken het mogelijk te reageren op framework- of applicatie-events en ontkoppelen businesslogica van de hoofdcode. ```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 { // Declareert de geabonneerde events en hun prioriteit public static function getSubscribedEvents(): array { return [ // Hoge prioriteit (vóór andere uitgevoerd) KernelEvents::EXCEPTION => ['onKernelException', 100], KernelEvents::RESPONSE => ['onKernelResponse', 0], ]; } public function onKernelException(ExceptionEvent $event): void { $exception = $event->getThrowable(); $request = $event->getRequest(); // Behandelt enkel 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); // Vervangt het antwoord door ons JSON $event->setResponse($response); } public function onKernelResponse(\Symfony\Component\HttpKernel\Event\ResponseEvent $event): void { // Voegt aangepaste headers toe $event->getResponse()->headers->set('X-Api-Version', '1.0'); } } ``` Event Subscribers worden automatisch ontdekt dankzij autoconfigure. De prioriteit bepaalt de uitvoeringsvolgorde (hoger = eerst uitgevoerd). ## Doctrine ORM ### Vraag 5: Leg de Doctrine-relaties en hun verschillen uit Doctrine biedt verschillende relatietypes om associaties tussen entiteiten te modelleren. Elk type heeft gevolgen voor queries en 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-relatie: een user heeft één profiel #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])] private ?Profile $profile = null; // OneToMany-relatie: een user heeft meerdere artikelen #[ORM\OneToMany(mappedBy: 'author', targetEntity: Article::class, orphanRemoval: true)] private Collection $articles; // ManyToMany-relatie: meerdere users hebben meerdere rollen #[ORM\ManyToMany(targetEntity: Role::class, inversedBy: 'users')] #[ORM\JoinTable(name: 'user_roles')] private Collection $roles; public function __construct() { // Verplichte initialisatie van de collection $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); // Bidirectionele synchronisatie } 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-relatie: meerdere artikelen behoren tot één auteur #[ORM\ManyToOne(inversedBy: 'articles')] #[ORM\JoinColumn(nullable: false)] private ?User $author = null; // ManyToMany met extra attributen via pivot-entiteit #[ORM\OneToMany(mappedBy: 'article', targetEntity: ArticleTag::class)] private Collection $articleTags; } ``` Bidirectionele relaties vereisen handmatige synchronisatie. De "owning"-zijde (met JoinColumn/JoinTable) bestuurt de persistentie. ### Vraag 6: Wat is het N+1-probleem en hoe los je het op met Doctrine? Het N+1-probleem ontstaat wanneer een hoofdquery N extra queries genereert om relaties te laden. Het is de meest voorkomende oorzaak van traagheid in Symfony-applicaties. ```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); } // SLECHT: N+1 queries als de auteurs benaderd worden public function findAllBad(): array { return $this->findAll(); // + 1 query per artikel om de auteur te laden } // GOED: JOIN met eager fetch public function findAllWithAuthor(): array { return $this->createQueryBuilder('a') ->addSelect('u') // SELECT ook van de auteur ->leftJoin('a.author', 'u') // JOIN op de relatie ->orderBy('a.publishedAt', 'DESC') ->getQuery() ->getResult(); } // GOED: meerdere JOINs voor meerdere relaties public function findAllWithDetails(): array { return $this->createQueryBuilder('a') ->addSelect('u', 'c', 't') // SELECT van alle relaties ->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(); } // GOED: batch-loading voor grote lijsten public function findAllOptimized(): array { $query = $this->createQueryBuilder('a') ->getQuery(); // Laadt relaties in batches van 100 $query->setFetchMode(Article::class, 'author', ClassMetadata::FETCH_BATCH); return $query->getResult(); } } ``` De Symfony Profiler met het Doctrine-paneel maakt N+1-problemen zichtbaar. Het aantal queries verschijnt in de Web Debug Toolbar. ### Vraag 7: Hoe maak je Query Extensions en Doctrine-filters? Query Extensions en Doctrine-filters maken het mogelijk voorwaarden automatisch toe te passen op alle queries — ideaal voor multi-tenancy of soft delete. ```php // src/Doctrine/Extension/CurrentUserExtension.php // API-Platform-extension om automatisch te filteren op gebruiker 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 { // Geldt alleen voor Article if ($resourceClass !== Article::class) { return; } // Admin ziet alles if ($this->security->isGranted('ROLE_ADMIN')) { return; } $user = $this->security->getUser(); $rootAlias = $queryBuilder->getRootAliases()[0]; // Automatische filter op auteur $queryBuilder ->andWhere(sprintf('%s.author = :current_user', $rootAlias)) ->setParameter('current_user', $user); } } ``` ```php // src/Doctrine/Filter/SoftDeleteFilter.php // Globale Doctrine-filter om verwijderde items uit te sluiten 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 { // Controleert of de entiteit een deletedAt-veld heeft 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 werken op SQL-niveau, extensions op QueryBuilder-niveau. Tijdelijk uitschakelen kan met `$em->getFilters()->disable('soft_delete')`. ## Symfony-beveiliging ### Vraag 8: Hoe werkt het Security-systeem van Symfony? De Security-component van Symfony beheert authenticatie (wie is de gebruiker) en autorisatie (wat mag hij doen) via een uitbreidbare architectuur. ```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 // Aangepaste authenticator voor specifieke logica 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 { // Deze authenticator behandelt alleen requests met 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 laadt de gebruiker op identifier return new SelfValidatingPassport( new UserBadge($apiKey, function (string $apiKey) { // Logica om gebruiker te laden via API-key return $this->userRepository->findByApiKey($apiKey); }) ); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { // null = request normaal voortzetten return null; } public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { return new JsonResponse([ 'error' => $exception->getMessage(), ], Response::HTTP_UNAUTHORIZED); } } ``` De security-architectuur steunt op Firewalls (configuratie), Authenticators (authenticatie) en Voters (autorisatie). ### Vraag 9: Hoe implementeer je Voters voor fijnmazige autorisatie? Voters maken complexe en herbruikbare autorisatielogica mogelijk en scheiden businessregels van controllercode. ```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 { // Deze voter behandelt alleen Article en deze attributen 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; // Gepubliceerde artikelen zijn voor iedereen zichtbaar if ($attribute === self::VIEW && $article->isPublished()) { return true; } // Andere acties vereisen een geauthenticeerde gebruiker 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 { // Concepten alleen zichtbaar voor auteur of admins return $article->getAuthor() === $user || in_array('ROLE_ADMIN', $user->getRoles()); } private function canEdit(Article $article, User $user): bool { // Alleen de auteur mag bewerken return $article->getAuthor() === $user; } private function canDelete(Article $article, User $user): bool { // Auteur of admin mogen verwijderen return $article->getAuthor() === $user || in_array('ROLE_ADMIN', $user->getRoles()); } } ``` ```php // src/Controller/ArticleController.php // Voter gebruiken in een 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 { // Autorisatie wordt automatisch gecontroleerd // 403 als de voter toegang weigert } // Programmatisch alternatief #[Route('/articles/{id}', name: 'article_show')] public function show(Article $article): Response { $this->denyAccessUnlessGranted(ArticleVoter::VIEW, $article); // Of met voorwaarde if (!$this->isGranted(ArticleVoter::EDIT, $article)) { // Bewerk-knop verbergen } } } ``` ```twig {# In Twig #} {% if is_granted('ARTICLE_EDIT', article) %} Bewerken {% endif %} ``` Voters worden automatisch ontdekt en geraadpleegd bij `isGranted()`-aanroepen. De standaardstrategie verleent toegang als minstens één Voter positief stemt. ### Vraag 10: Hoe beveilig je een API met JWT in Symfony? JWT-authenticatie (JSON Web Token) is de standaardoplossing voor stateless API's. Symfony gebruikt doorgaans het 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 uur ``` ```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); } // JWT-token genereren $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 { // Door het bundle automatisch afgehandeld als geconfigureerd // Levert een nieuw token op basis van het refresh token } } ``` ```php // src/EventListener/JWTCreatedListener.php // Aanpassing van de JWT-payload namespace App\EventListener; use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent; class JWTCreatedListener { public function onJWTCreated(JWTCreatedEvent $event): void { $user = $event->getUser(); $payload = $event->getData(); // Voegt aangepaste data toe aan het token $payload['user_id'] = $user->getId(); $payload['email'] = $user->getEmail(); $payload['permissions'] = $user->getPermissions(); $event->setData($payload); } } ``` Het JWT-token wordt verzonden in de header `Authorization: Bearer `. Het bundle verifieert automatisch handtekening en vervaltijd. ## Symfony-formulieren ### Vraag 11: Hoe maak je geavanceerde formulieren met validatie? De Form-component van Symfony genereert HTML-formulieren, behandelt verzending en valideert data via 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' => 'Titel van het artikel', 'attr' => ['placeholder' => 'Voer de titel in...'], 'constraints' => [ new Assert\NotBlank(message: 'Titel is verplicht'), new Assert\Length( min: 10, max: 255, minMessage: 'Titel moet minstens {{ limit }} tekens zijn', ), ], ]) ->add('content', TextareaType::class, [ 'label' => 'Inhoud', 'constraints' => [ new Assert\NotBlank(), new Assert\Length(min: 100), ], ]) ->add('category', EntityType::class, [ 'class' => Category::class, 'choice_label' => 'name', 'placeholder' => 'Kies een categorie', 'query_builder' => function ($repo) { return $repo->createQueryBuilder('c') ->where('c.active = true') ->orderBy('c.name', 'ASC'); }, ]) ->add('coverImage', FileType::class, [ 'label' => 'Omslagafbeelding', 'mapped' => false, // Niet gekoppeld aan de entiteit 'required' => false, 'constraints' => [ new Assert\Image( maxSize: '5M', mimeTypes: ['image/jpeg', 'image/png', 'image/webp'], mimeTypesMessage: 'Niet-ondersteund afbeeldingsformaat', ), ], ]) ->add('publishedAt', DateTimeType::class, [ 'widget' => 'single_text', 'required' => false, 'label' => 'Publicatiedatum', ]); // Event listener om het formulier dynamisch aan te passen $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { $article = $event->getData(); $form = $event->getForm(); // Veld alleen toevoegen bij bewerken if ($article && $article->getId()) { $form->add('slug', TextType::class, [ 'disabled' => true, 'help' => 'De slug kan niet worden gewijzigd', ]); } }); } 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()) { // Bestandsupload afhandelen $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', 'Artikel succesvol aangemaakt!'); return $this->redirectToRoute('article_show', ['id' => $article->getId()]); } return $this->render('article/new.html.twig', [ 'form' => $form, ]); } ``` FormEvents (`PRE_SET_DATA`, `POST_SUBMIT`, enz.) maken het mogelijk velden dynamisch aan te passen op basis van context. ### Vraag 12: Hoe implementeer je aangepaste validatie met constraints? Symfony laat toe aangepaste validatie-constraints te maken voor complexe businessregels. ```php // src/Validator/UniqueEmail.php // Aangepaste constraint namespace App\Validator; use Symfony\Component\Validator\Constraint; #[\Attribute(\Attribute::TARGET_PROPERTY)] class UniqueEmail extends Constraint { public string $message = 'Het e-mailadres "{{ value }}" is al in gebruik.'; public ?int $excludeId = null; // Om de huidige gebruiker bij update uit te sluiten } ``` ```php // src/Validator/UniqueEmailValidator.php // Validator gekoppeld aan de 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; // NotBlank verwerkt lege waarden } $existingUser = $this->userRepository->findOneBy(['email' => $value]); // Controleert of er een gebruiker met dit e-mailadres bestaat // en niet de huidige gebruiker is (bij update) if ($existingUser && $existingUser->getId() !== $constraint->excludeId) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $value) ->addViolation(); } } } ``` ```php // src/Entity/User.php // Constraint gebruiken op de entiteit 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: 'Wachtwoord moet hoofdletter, kleine letter en cijfer bevatten' )] private ?string $plainPassword = null; } ``` ```php // Constraint op klasseniveau voor multi-veld-validatie // src/Validator/PasswordMatch.php #[\Attribute(\Attribute::TARGET_CLASS)] class PasswordMatch extends Constraint { public string $message = 'De wachtwoorden komen niet overeen.'; public function getTargets(): string { return self::CLASS_CONSTRAINT; } } ``` Aangepaste constraints worden automatisch ontdekt. Het achtervoegsel `Validator` is verplicht voor de validator. ## Messenger en asynchrone communicatie ### Vraag 13: Hoe implementeer je asynchrone verwerking met Messenger? Symfony Messenger verzendt berichten naar queues voor asynchrone verwerking en verbetert zo de responsiviteit van de applicatie. ```php // src/Message/SendWelcomeEmail.php // Het bericht (DTO met de data) namespace App\Message; final class SendWelcomeEmail { public function __construct( public readonly int $userId, public readonly string $locale = 'en', ) {} } ``` ```php // src/MessageHandler/SendWelcomeEmailHandler.php // De handler die het bericht verwerkt 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; // Gebruiker is intussen verwijderd } $email = (new TemplatedEmail()) ->to($user->getEmail()) ->subject('Welkom op ons 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: # Berichten naar het async transport routeren App\Message\SendWelcomeEmail: async App\Message\ProcessImage: async App\Message\GenerateReport: async ``` ```php // src/Controller/RegistrationController.php // Verzending van het bericht use Symfony\Component\Messenger\MessageBusInterface; class RegistrationController extends AbstractController { #[Route('/register', name: 'app_register', methods: ['POST'])] public function register( Request $request, MessageBusInterface $bus, ): Response { // ... gebruiker aanmaken // Asynchrone dispatch - e-mail wordt op de achtergrond verzonden $bus->dispatch(new SendWelcomeEmail( userId: $user->getId(), locale: $request->getLocale(), )); // Onmiddellijk antwoord aan de gebruiker return $this->redirectToRoute('app_login'); } } ``` Start de worker met `php bin/console messenger:consume async -vv`. In productie houdt Supervisor de worker actief. ### Vraag 14: Hoe behandel je fouten en retries met Messenger? Messenger biedt robuuste mechanismen om fouten af te handelen: automatische retry, dead letter queue en handmatige verwerking van mislukte berichten. ```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) { // Tijdelijke fout (timeout, rate limit) → retry throw new RecoverableMessageHandlingException( $e->getMessage(), previous: $e ); } catch (PaymentFailedException $e) { // Permanente fout (ongeldige kaart) → geen retry throw new UnrecoverableMessageHandlingException( $e->getMessage(), previous: $e ); } } } ``` ```php // src/Message/ProcessPayment.php // Retry-configuratie op berichtniveau namespace App\Message; use Symfony\Component\Messenger\Stamp\DelayStamp; final class ProcessPayment { public function __construct( public readonly int $orderId, public readonly int $attempt = 1, ) {} // Aangepaste retry-vertraging op basis van het aantal pogingen public function getRetryDelay(): int { return match ($this->attempt) { 1 => 5000, // 5 seconden 2 => 30000, // 30 seconden 3 => 300000, // 5 minuten default => 600000, }; } } ``` ```bash # Commando's voor het beheren van mislukte berichten php bin/console messenger:failed:show # Mislukte berichten tonen php bin/console messenger:failed:retry # Alle berichten opnieuw proberen php bin/console messenger:failed:retry 123 # Specifiek bericht opnieuw proberen php bin/console messenger:failed:remove 123 # Een bericht verwijderen ``` De retry-strategie en het "failed"-transport zorgen dat geen enkel bericht verloren gaat. Berichten kunnen worden geanalyseerd en handmatig opnieuw verzonden. ## Tests in Symfony ### Vraag 15: Hoe structureer je tests in Symfony? Symfony levert PHPUnit met dedicated helpers om de verschillende lagen van de applicatie te testen: unit-, functionele en integratietests. ```php // tests/Unit/Service/PriceCalculatorTest.php // Unittest: test een geïsoleerde klasse 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 // Functionele test: test 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 { // Authenticatie $user = $this->createUser(); $this->client->loginUser($user); // Toegang tot het formulier $crawler = $this->client->request('GET', '/articles/new'); $this->assertResponseIsSuccessful(); // Formulier verzenden $form = $crawler->selectButton('Aanmaken')->form([ 'article[title]' => 'Test Article Title', 'article[content]' => 'Dit is de inhoud van mijn testartikel met voldoende tekens.', ]); $this->client->submit($form); // Verificatie $this->assertResponseRedirects(); $this->client->followRedirect(); $this->assertSelectorTextContains('h1', 'Test Article Title'); // Verificatie in de database $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 { // Test-database opruimen $this->entityManager->getConnection()->executeStatement('DELETE FROM article'); $this->entityManager->getConnection()->executeStatement('DELETE FROM user'); parent::tearDown(); } } ``` Het is verstandig unittests (zonder kernel), functionele tests (met kernel) en integratietests (echte services) te scheiden. ### Vraag 16: Hoe gebruik je fixtures en DatabaseResetter? Fixtures vullen de database met realistische testdata. Het component DoctrineTestBundle vergemakkelijkt het resetten tussen 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("Gedetailleerde inhoud van artikel nummer $i..."); $article->setStatus($i <= 15 ? 'published' : 'draft'); $article->setPublishedAt($i <= 15 ? new \DateTimeImmutable("-$i days") : null); // Verwijzing naar gebruiker aangemaakt door UserFixtures $article->setAuthor($this->getReference('user-'.($i % 3), User::class)); $manager->persist($article); // Referenties aanmaken voor andere fixtures $this->addReference("article-$i", $article); } $manager->flush(); } public function getDependencies(): array { // UserFixtures moet vóór ArticleFixtures geladen worden 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 // DAMADoctrineTestBundle gebruiken voor automatische 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); // Volgens de fixtures } } ``` ```bash # Fixtures laden php bin/console doctrine:fixtures:load php bin/console doctrine:fixtures:load --group=test php bin/console doctrine:fixtures:load --env=test ``` DAMADoctrineTestBundle wikkelt elke test in een transactie die wordt teruggerold, zodat fixtures niet opnieuw geladen hoeven worden tussen tests. ## Architectuur en geavanceerde patterns ### Vraag 17: Hoe implementeer je CQRS met Symfony? CQRS (Command Query Responsibility Segregation) scheidt lees- van schrijfoperaties en maakt onafhankelijke optimalisatie mogelijk. ```php // src/Message/Command/CreateArticleCommand.php // Command: vertegenwoordigt een wijzigingsintentie 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: voert de wijziging uit 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: vertegenwoordigt een leesverzoek 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: haalt data op (kan een geoptimaliseerd read model gebruiken) 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 // Command- en Query-bus gebruiken 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 maakt het mogelijk lezen (caching, projecties) en schrijven (validatie, events) afzonderlijk te optimaliseren. ### Vraag 18: Hoe implementeer je het Repository Pattern correct in Symfony? Het Repository Pattern is in Symfony al aanwezig via Doctrine, maar kan worden uitgebreid met interfaces en businessmethodes. ```php // src/Repository/Contract/ArticleRepositoryInterface.php // Interface voor ontkoppeling en testbaarheid 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-implementatie van de 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(); } } // Methodes voor complexe queries 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 van interface aan implementatie services: App\Repository\Contract\ArticleRepositoryInterface: alias: App\Repository\ArticleRepository ``` De interface laat toe testimplementaties te maken (InMemoryArticleRepository) of de databron te wijzigen zonder de businesscode aan te passen. ### Vraag 19: Hoe beheer je configuratie en omgevingen in Symfony? Symfony gebruikt een flexibel configuratiesysteem met ondersteuning voor omgevingsvariabelen, secrets en YAML-bestanden per omgeving. ```yaml # config/packages/framework.yaml # Standaardconfiguratie 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 voor productie 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 // Beheer van gevoelige (versleutelde) secrets // php bin/console secrets:set DATABASE_URL --env=prod // php bin/console secrets:list --reveal --env=prod ``` ```php // src/DependencyInjection/Configuration.php // Aangepaste configuratie voor een 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 // Configuratie-injectie 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 zijn versleuteld en versiebeheerd. Gebruik `%env(...)%` voor runtime-variabelen en parameters voor statische waarden. ## Performance en productie ### Vraag 20: Hoe optimaliseer je de performance van een Symfony-applicatie? Optimalisatie omvat verschillende niveaus: opcache, configuratie, applicatie-cache en Doctrine-queries. ```php // config/packages/prod/doctrine.yaml # Geoptimaliseerde Doctrine-configuratie voor productie 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-configuratie met 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 // Doctrine result cache gebruiken 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 1u ->getResult(); } ``` ```bash # Optimalisatiecommando's voor productie 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 # Doctrine-proxies genereren php bin/console doctrine:proxy:create-proxy-classes # Geoptimaliseerde autoloader compileren composer install --no-dev --optimize-autoloader --classmap-authoritative ``` OPcache moet in productie ingeschakeld zijn met optimale instellingen. De warmup genereert de container- en routercache. ### Vraag 21: Hoe configureer je logging en monitoring in Symfony? Gestructureerde logging en goede monitoring zijn essentieel om problemen in productie te diagnosticeren. ```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 // Gestructureerde logging met 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 // Logging van HTTP-requests 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-formaat vergemakkelijkt opname door monitoringtools (ELK, Datadog). Channels laten filtering per logtype toe. ### Vraag 22: Hoe deploy je een Symfony-applicatie naar productie? Een robuuste Symfony-deployment combineert build-voorbereiding, veilige migraties en atomische omschakeling. ```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 "Release-directory aanmaken..." mkdir -p $RELEASE_DIR echo "Repository klonen..." git clone --depth 1 --branch main git@github.com:org/repo.git $RELEASE_DIR echo "Dependencies installeren..." cd $RELEASE_DIR composer install --no-dev --optimize-autoloader --classmap-authoritative echo "Gedeelde bestanden linken..." 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 "Migraties uitvoeren..." php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration echo "Cache opwarmen..." php bin/console cache:clear --env=prod --no-debug php bin/console cache:warmup --env=prod --no-debug echo "Permissies instellen..." chown -R www-data:www-data $RELEASE_DIR echo "Omschakelen naar nieuwe release..." ln -sfn $RELEASE_DIR $CURRENT_LINK echo "PHP-FPM herstarten..." sudo systemctl reload php8.3-fpm echo "Messenger-workers herstarten..." php bin/console messenger:stop-workers echo "Oude releases opruimen..." ls -dt /var/www/releases/*/ | tail -n +6 | xargs rm -rf echo "Deployment voltooid!" ``` ```yaml # .github/workflows/deploy.yml # CI/CD-deployment met 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 ``` Atomische deployment via symlinks staat een onmiddellijke rollback toe. Messenger-workers moeten worden herstart om de nieuwe code te laden. ## API Platform en REST ### Vraag 23: Hoe bouw je een REST API met API Platform? API Platform is de standaardoplossing om REST- en GraphQL-API's te bouwen met Symfony, met automatische documentatie en HTTP-standaarden. ```php // src/Entity/Article.php // Configuratie van een API Platform-resource 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 // Aangepaste processor voor businesslogica 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()) { // Nieuw artikel: auteur en slug instellen $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 genereert automatisch OpenAPI-documentatie en biedt filters, paginatie en validatie. ### Vraag 24: Hoe pas je API Platform-operaties aan? API Platform laat toe aangepaste operaties te maken met controllers of State Providers/Processors. ```php // src/Entity/Article.php // Aangepaste operaties #[ApiResource( operations: [ // Standaardoperaties new GetCollection(), new Get(), // Aangepaste operatie met controller new Post( uriTemplate: '/articles/{id}/publish', controller: PublishArticleController::class, openapi: new Model\Operation( summary: 'Publiceert een artikel', description: 'Wijzigt de artikelstatus naar "published"', ), security: "is_granted('ARTICLE_EDIT', object)", ), // Operatie met aangepaste State Provider new GetCollection( uriTemplate: '/articles/trending', provider: TrendingArticlesProvider::class, openapiContext: ['summary' => 'Trending artikelen'], ), ], )] class Article { // ... } ``` ```php // src/Controller/PublishArticleController.php // Controller voor aangepaste operatie 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('Artikel al gepubliceerd'); } $article->setStatus('published'); $article->setPublishedAt(new \DateTimeImmutable()); $this->entityManager->flush(); return $article; } } ``` ```php // src/State/TrendingArticlesProvider.php // State Provider voor aangepaste leeslogica 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 { // Aangepaste logica voor trending artikelen 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 verzorgen het lezen, State Processors het schrijven. Controllers blijven beschikbaar voor complexe gevallen. ### Vraag 25: Hoe beheer je databasemigraties in productie? Doctrine-migraties moeten ontworpen zijn om zonder downtime uit te voeren en eenvoudig terug te draaien. ```php // migrations/Version20260202100000.php // Veilige migratie: nullable kolom toevoegen 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 { // Stap 1: nullable kolom toevoegen $this->addSql('ALTER TABLE users ADD role VARCHAR(50) DEFAULT NULL'); // Index aanmaken met CONCURRENTLY (PostgreSQL - niet-blokkerend) $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 // Datamigratie (apart) final class Version20260202100001 extends AbstractMigration { public function getDescription(): string { return 'Populate role column with default value'; } public function up(Schema $schema): void { // Batchmigratie voor grote tabellen $this->addSql("UPDATE users SET role = 'ROLE_USER' WHERE role IS NULL"); } public function down(Schema $schema): void { // Geen rollback nodig voor de data } } ``` ```php // migrations/Version20260202100002.php // Laatste migratie: kolom NOT NULL maken final class Version20260202100002 extends AbstractMigration { public function getDescription(): string { return 'Make role column NOT NULL'; } public function up(Schema $schema): void { // Voorafgaande controle $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 # Commando's voor migratiebeheer php bin/console doctrine:migrations:status # Status van migraties php bin/console doctrine:migrations:migrate # Migraties uitvoeren php bin/console doctrine:migrations:migrate prev # Laatste migratie terugdraaien php bin/console doctrine:migrations:diff # Migratie genereren uit schema php bin/console doctrine:migrations:execute --down # Specifieke rollback ``` De expand-contract strategie (3 deployments) garandeert een zero-downtime deployment: nullable toevoegen → data migreren → constraint toevoegen. ## Conclusie Deze 25 vragen dekken het essentiële van Symfony-sollicitatiegesprekken, van de fundamenten van de Service Container tot geavanceerde productiepatronen. Voorbereidingschecklist: - ✅ Service Container en dependency injection - ✅ Doctrine ORM: relaties, queries, filters - ✅ Beveiliging: authenticatie, voters, JWT - ✅ Formulieren en aangepaste validatie - ✅ Messenger: asynchrone verwerking en foutafhandeling - ✅ Tests: unit, functioneel, fixtures - ✅ Architectuur: CQRS, Repository Pattern - ✅ API Platform: REST, aangepaste operaties - ✅ Productie: performance, logging, deployment > **Verder verdiepen** > > Elke vraag verdient een diepere studie met de officiële Symfony-documentatie. Recruiters waarderen kandidaten die de architectonische keuzes van het framework begrijpen en hun technische beslissingen kunnen onderbouwen. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/symfony/symfony-interview-questions