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.

Symfony- en PHP-sollicitatievragen - Volledige gids

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.

public/index.phpphp
// 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']);
};
src/Kernel.phpphp
// 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.

src/Service/PaymentService.phpphp
// 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.

src/MyBundle/MyBundle.phpphp
// 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.

src/EventSubscriber/ApiExceptionSubscriber.phpphp
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.

src/Entity/User.phpphp
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;
    }
}
src/Entity/Article.phpphp
#[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.

src/Repository/ArticleRepository.phpphp
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.

src/Doctrine/Extension/CurrentUserExtension.phpphp
// 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);
    }
}
src/Doctrine/Filter/SoftDeleteFilter.phpphp
// 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').

Klaar om je Symfony gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

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 }
src/Security/CustomAuthenticator.phpphp
// 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.

src/Security/Voter/ArticleVoter.phpphp
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());
    }
}
src/Controller/ArticleController.phpphp
// 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) %}
    <a href="{{ path('article_edit', {id: article.id}) }}">Bewerken</a>
{% 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
src/Controller/Api/AuthController.phpphp
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
    }
}
src/EventListener/JWTCreatedListener.phpphp
// 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 <token>. 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.

src/Form/ArticleType.phpphp
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'],
        ]);
    }
}
src/Controller/ArticleController.phpphp
#[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.

src/Validator/UniqueEmail.phpphp
// 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
}
src/Validator/UniqueEmailValidator.phpphp
// 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();
        }
    }
}
src/Entity/User.phpphp
// 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.

src/Message/SendWelcomeEmail.phpphp
// Het bericht (DTO met de data)
namespace App\Message;

final class SendWelcomeEmail
{
    public function __construct(
        public readonly int $userId,
        public readonly string $locale = 'en',
    ) {}
}
src/MessageHandler/SendWelcomeEmailHandler.phpphp
// 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
src/Controller/RegistrationController.phpphp
// 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.

src/MessageHandler/ProcessPaymentHandler.phpphp
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
            );
        }
    }
}
src/Message/ProcessPayment.phpphp
// 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.

Klaar om je Symfony gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

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.

tests/Unit/Service/PriceCalculatorTest.phpphp
// 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],
        ];
    }
}
tests/Functional/Controller/ArticleControllerTest.phpphp
// 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.

src/DataFixtures/ArticleFixtures.phpphp
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,
        ];
    }
}
src/DataFixtures/UserFixtures.phpphp
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'];
    }
}
tests/Functional/ArticleControllerTest.phpphp
// 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.

src/Message/Command/CreateArticleCommand.phpphp
// 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 = [],
    ) {}
}
src/MessageHandler/Command/CreateArticleCommandHandler.phpphp
// 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;
    }
}
src/Message/Query/GetArticleBySlugQuery.phpphp
// Query: vertegenwoordigt een leesverzoek
namespace App\Message\Query;

final class GetArticleBySlugQuery
{
    public function __construct(
        public readonly string $slug,
        public readonly bool $withComments = false,
    ) {}
}
src/MessageHandler/Query/GetArticleBySlugQueryHandler.phpphp
// 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;
    }
}
src/Controller/ArticleController.phpphp
// 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.

src/Repository/Contract/ArticleRepositoryInterface.phpphp
// 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;
}
src/Repository/ArticleRepository.phpphp
// 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
config/secrets/prod/prod.decrypt.private.phpphp
// Beheer van gevoelige (versleutelde) secrets
// php bin/console secrets:set DATABASE_URL --env=prod
// php bin/console secrets:list --reveal --env=prod
src/DependencyInjection/Configuration.phpphp
// 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;
    }
}
src/Service/ConfigurableService.phpphp
// 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.

config/packages/prod/doctrine.yamlphp
# 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'
src/Repository/ArticleRepository.phpphp
// 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'
src/Service/PaymentService.phpphp
// 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;
        }
    }
}
src/EventSubscriber/RequestLoggerSubscriber.phpphp
// 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.

src/Entity/Article.phpphp
// 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;
}
src/State/ArticleProcessor.phpphp
// 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.

src/Entity/Article.phpphp
// 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
{
    // ...
}
src/Controller/PublishArticleController.phpphp
// 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;
    }
}
src/State/TrendingArticlesProvider.phpphp
// 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.

migrations/Version20260202100000.phpphp
// 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');
    }
}
migrations/Version20260202100001.phpphp
// 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
    }
}
migrations/Version20260202100002.phpphp
// 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.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

#symfony
#php
#interview
#doctrine
#technical interview

Delen

Gerelateerde artikelen