Symfony-Interviewfragen: Top 25 in 2026
Die 25 häufigsten Symfony-Interviewfragen. Architektur, Doctrine ORM, Services, Security, Formulare und Tests mit ausführlichen Antworten und Codebeispielen.

Symfony-Interviews prüfen die Beherrschung des professionellen PHP-Referenzframeworks, das Verständnis komponentenorientierter Architektur, Doctrine ORM und die Fähigkeit, robuste und skalierbare Anwendungen zu bauen. Dieser Leitfaden behandelt die 25 häufigsten Fragen, von Symfony-Grundlagen bis zu fortgeschrittenen Produktionspatterns.
Recruiter schätzen Kandidaten, die die Symfony-Philosophie verstehen: Entkopplung über Services, explizite Konfiguration und Einhaltung der PSR-Standards. Die architektonischen Entscheidungen des Frameworks erklären zu können, macht den Unterschied.
Symfony-Grundlagen
Frage 1: Erkläre den Request-Lebenszyklus in Symfony
Der Request-Lebenszyklus von Symfony durchläuft den HTTP Kernel und nutzt ein Event-System, um Erweiterungen an jedem Schritt zu ermöglichen. Dieses Verständnis ist essenziell für Debugging und Anpassung des Anwendungsverhaltens.
// Einstiegspunkt für alle HTTP-Requests
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
// Symfony Runtime übernimmt das Bootstrap
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};// Der Kernel orchestriert die Request-Verarbeitung
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
// Der Kernel lädt Bundles und konfiguriert den Container
// Schlüssel-Events des Lebenszyklus:
// 1. kernel.request - Vor dem Routing
// 2. kernel.controller - Nach Controller-Auflösung
// 3. kernel.view - Wenn der Controller keine Response liefert
// 4. kernel.response - Vor dem Senden der Response
// 5. kernel.terminate - Nach dem Senden (asynchrone Tasks)
}Der vollständige Zyklus: index.php → Runtime → Kernel → HttpKernel → EventDispatcher (kernel.request) → Router → Controller → EventDispatcher (kernel.response) → Response. Jeder Schritt lässt sich über Event Subscriber abfangen.
Frage 2: Was sind der Service Container und Dependency Injection in Symfony?
Der Service Container (oder DIC, Dependency Injection Container) ist das Herzstück von Symfony. Er verwaltet Instanziierung, Konfiguration und Injection aller Anwendungsservices.
// Service mit automatisch injizierten Abhängigkeiten
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, // Konfigurierter HTTP-Client
private readonly OrderRepository $orderRepository, // Doctrine-Repository
private readonly LoggerInterface $logger, // PSR-3-Logger
private readonly string $stripeApiKey, // Injizierter Parameter
) {}
public function processPayment(int $orderId, float $amount): bool
{
$order = $this->orderRepository->find($orderId);
try {
$response = $this->stripeClient->request('POST', '/charges', [
'body' => [
'amount' => $amount * 100,
'currency' => 'eur',
'source' => $order->getPaymentToken(),
],
]);
$order->markAsPaid($response->toArray()['id']);
$this->orderRepository->save($order, true);
return true;
} catch (\Exception $e) {
$this->logger->error('Payment failed', [
'order' => $orderId,
'error' => $e->getMessage(),
]);
return false;
}
}
}# config/services.yaml
# Service-Konfiguration
services:
_defaults:
autowire: true # Automatische Injection per Type-Hint
autoconfigure: true # Automatische Tag-Konfiguration
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# Explizite Konfiguration mit Parametern
App\Service\PaymentService:
arguments:
$stripeClient: '@stripe.client'
$stripeApiKey: '%env(STRIPE_API_KEY)%'Autowiring löst Abhängigkeiten automatisch per Type-Hint auf. Skalare Parameter erfordern explizite Konfiguration.
Frage 3: Worin unterscheiden sich ein Bundle und eine Symfony-Komponente?
Bundles sind wiederverwendbare Pakete, die Funktionalitäten in eine Symfony-Anwendung integrieren. Komponenten sind eigenständige PHP-Bibliotheken, die ohne Symfony nutzbar sind.
// Struktur eines benutzerdefinierten Bundles
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
{
// Lädt die Bundle-Konfiguration
public function loadExtension(
array $config,
ContainerConfigurator $container,
ContainerBuilder $builder
): void {
// Lädt die Bundle-Services
$container->import('../config/services.yaml');
// Bedingte Konfiguration
if ($config['feature_enabled']) {
$container->services()
->set('my_bundle.feature_service', FeatureService::class)
->autowire();
}
}
// Standardkonfiguration des Bundles
public function configure(DefinitionConfigurator $definition): void
{
$definition->rootNode()
->children()
->booleanNode('feature_enabled')->defaultTrue()->end()
->scalarNode('api_key')->isRequired()->end()
->end();
}
}// Nutzung einer Komponente ohne Symfony
// Komponenten sind eigenständige PHP-Bibliotheken
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// In jedem PHP-Projekt verwendbar
$request = Request::createFromGlobals();
$response = new Response('Hello World', 200);
$response->send();Bundles kapseln Konfiguration, Services und Ressourcen. Komponenten sind Low-Level-Werkzeuge, die überall wiederverwendet werden können.
Frage 4: Wie funktionieren Event Subscriber in Symfony?
Event Subscriber erlauben es, auf Framework- oder Anwendungsevents zu reagieren und entkoppeln Geschäftslogik vom Hauptcode.
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
{
// Deklariert die abonnierten Events und ihre Priorität
public static function getSubscribedEvents(): array
{
return [
// Hohe Priorität (vor anderen ausgeführt)
KernelEvents::EXCEPTION => ['onKernelException', 100],
KernelEvents::RESPONSE => ['onKernelResponse', 0],
];
}
public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
$request = $event->getRequest();
// Behandelt nur 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);
// Ersetzt die Response durch unser JSON
$event->setResponse($response);
}
public function onKernelResponse(\Symfony\Component\HttpKernel\Event\ResponseEvent $event): void
{
// Fügt benutzerdefinierte Header hinzu
$event->getResponse()->headers->set('X-Api-Version', '1.0');
}
}Event Subscriber werden dank autoconfigure automatisch erkannt. Die Priorität bestimmt die Ausführungsreihenfolge (höher = zuerst ausgeführt).
Doctrine ORM
Frage 5: Erkläre die Doctrine-Beziehungen und ihre Unterschiede
Doctrine bietet mehrere Beziehungstypen, um Verknüpfungen zwischen Entitäten zu modellieren. Jeder Typ hat Auswirkungen auf Queries und Performance.
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
// OneToOne-Beziehung: ein User hat ein Profil
#[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])]
private ?Profile $profile = null;
// OneToMany-Beziehung: ein User hat mehrere Artikel
#[ORM\OneToMany(mappedBy: 'author', targetEntity: Article::class, orphanRemoval: true)]
private Collection $articles;
// ManyToMany-Beziehung: mehrere User haben mehrere Rollen
#[ORM\ManyToMany(targetEntity: Role::class, inversedBy: 'users')]
#[ORM\JoinTable(name: 'user_roles')]
private Collection $roles;
public function __construct()
{
// Pflichtinitialisierung der 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); // Bidirektionale Synchronisierung
}
return $this;
}
public function removeArticle(Article $article): static
{
if ($this->articles->removeElement($article)) {
if ($article->getAuthor() === $this) {
$article->setAuthor(null);
}
}
return $this;
}
}#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
// ManyToOne-Beziehung: mehrere Artikel gehören zu einem Autor
#[ORM\ManyToOne(inversedBy: 'articles')]
#[ORM\JoinColumn(nullable: false)]
private ?User $author = null;
// ManyToMany mit zusätzlichen Attributen über Pivot-Entität
#[ORM\OneToMany(mappedBy: 'article', targetEntity: ArticleTag::class)]
private Collection $articleTags;
}Bidirektionale Beziehungen erfordern manuelle Synchronisierung. Die "Owning"-Seite (mit JoinColumn/JoinTable) steuert die Persistierung.
Frage 6: Was ist das N+1-Problem und wie löst man es mit Doctrine?
Das N+1-Problem entsteht, wenn eine Hauptquery N zusätzliche Queries zum Laden der Beziehungen erzeugt. Es ist die häufigste Ursache für Langsamkeit in Symfony-Anwendungen.
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);
}
// SCHLECHT: N+1 Queries beim Zugriff auf die Autoren
public function findAllBad(): array
{
return $this->findAll();
// + 1 Query pro Artikel zum Laden des Autors
}
// GUT: JOIN mit Eager Fetch
public function findAllWithAuthor(): array
{
return $this->createQueryBuilder('a')
->addSelect('u') // SELECT auch des Autors
->leftJoin('a.author', 'u') // JOIN auf der Beziehung
->orderBy('a.publishedAt', 'DESC')
->getQuery()
->getResult();
}
// GUT: mehrere JOINs für mehrere Beziehungen
public function findAllWithDetails(): array
{
return $this->createQueryBuilder('a')
->addSelect('u', 'c', 't') // SELECT aller Beziehungen
->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();
}
// GUT: Batch-Loading für große Listen
public function findAllOptimized(): array
{
$query = $this->createQueryBuilder('a')
->getQuery();
// Lädt Beziehungen in Batches von 100
$query->setFetchMode(Article::class, 'author', ClassMetadata::FETCH_BATCH);
return $query->getResult();
}
}Der Symfony Profiler mit dem Doctrine-Panel hilft, N+1-Probleme zu erkennen. Die Query-Anzahl erscheint in der Web Debug Toolbar.
Frage 7: Wie erstellt man Query Extensions und Doctrine-Filter?
Query Extensions und Doctrine-Filter erlauben es, Bedingungen automatisch auf alle Queries anzuwenden — ideal für Multi-Tenancy oder Soft Delete.
// API-Platform-Extension zum automatischen Filtern nach Benutzer
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
{
// Gilt nur für Article
if ($resourceClass !== Article::class) {
return;
}
// Admin sieht alles
if ($this->security->isGranted('ROLE_ADMIN')) {
return;
}
$user = $this->security->getUser();
$rootAlias = $queryBuilder->getRootAliases()[0];
// Automatischer Filter nach Autor
$queryBuilder
->andWhere(sprintf('%s.author = :current_user', $rootAlias))
->setParameter('current_user', $user);
}
}// Globaler Doctrine-Filter zum Ausschluss gelöschter Einträge
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
{
// Prüft, ob die Entität ein deletedAt-Feld besitzt
if (!$targetEntity->hasField('deletedAt')) {
return '';
}
return sprintf('%s.deleted_at IS NULL', $targetTableAlias);
}
}# config/packages/doctrine.yaml
doctrine:
orm:
filters:
soft_delete:
class: App\Doctrine\Filter\SoftDeleteFilter
enabled: trueFilter wirken auf SQL-Ebene, Extensions auf QueryBuilder-Ebene. Vorübergehend deaktivierbar mit $em->getFilters()->disable('soft_delete').
Bereit für deine Symfony-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Symfony Security
Frage 8: Wie funktioniert das Security-System von Symfony?
Die Security-Komponente von Symfony verwaltet Authentifizierung (wer ist der Benutzer) und Autorisierung (was darf er) über eine erweiterbare Architektur.
# 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 }// Benutzerdefinierter Authenticator für spezifische Logik
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
{
// Dieser Authenticator behandelt nur Requests mit 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 lädt den Benutzer per Identifier
return new SelfValidatingPassport(
new UserBadge($apiKey, function (string $apiKey) {
// Logik zum Laden des Benutzers per API-Key
return $this->userRepository->findByApiKey($apiKey);
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// null = Request normal fortsetzen
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new JsonResponse([
'error' => $exception->getMessage(),
], Response::HTTP_UNAUTHORIZED);
}
}Die Security-Architektur basiert auf Firewalls (Konfiguration), Authenticators (Authentifizierung) und Voters (Autorisierung).
Frage 9: Wie implementiert man Voter für feingranulare Autorisierung?
Voter ermöglichen komplexe und wiederverwendbare Autorisierungslogik und trennen Geschäftsregeln vom Controller-Code.
namespace App\Security\Voter;
use App\Entity\Article;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class ArticleVoter extends Voter
{
public const VIEW = 'ARTICLE_VIEW';
public const EDIT = 'ARTICLE_EDIT';
public const DELETE = 'ARTICLE_DELETE';
protected function supports(string $attribute, mixed $subject): bool
{
// Dieser Voter behandelt nur Article und diese Attribute
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;
// Veröffentlichte Artikel sind für alle sichtbar
if ($attribute === self::VIEW && $article->isPublished()) {
return true;
}
// Andere Aktionen erfordern einen authentifizierten Benutzer
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
{
// Entwürfe nur für Autor oder Admins sichtbar
return $article->getAuthor() === $user
|| in_array('ROLE_ADMIN', $user->getRoles());
}
private function canEdit(Article $article, User $user): bool
{
// Nur der Autor darf bearbeiten
return $article->getAuthor() === $user;
}
private function canDelete(Article $article, User $user): bool
{
// Autor oder Admin dürfen löschen
return $article->getAuthor() === $user
|| in_array('ROLE_ADMIN', $user->getRoles());
}
}// Nutzung des Voters in einem 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
{
// Die Autorisierung wird automatisch geprüft
// 403, falls der Voter den Zugriff verweigert
}
// Programmatische Alternative
#[Route('/articles/{id}', name: 'article_show')]
public function show(Article $article): Response
{
$this->denyAccessUnlessGranted(ArticleVoter::VIEW, $article);
// Oder mit Bedingung
if (!$this->isGranted(ArticleVoter::EDIT, $article)) {
// Edit-Button verbergen
}
}
}{# In Twig #}
{% if is_granted('ARTICLE_EDIT', article) %}
<a href="{{ path('article_edit', {id: article.id}) }}">Bearbeiten</a>
{% endif %}Voter werden automatisch erkannt und bei isGranted()-Aufrufen herangezogen. Die Standardstrategie gewährt Zugriff, wenn mindestens ein Voter zustimmt.
Frage 10: Wie sichert man eine API mit JWT in Symfony?
Die JWT-Authentifizierung (JSON Web Token) ist die Standardlösung für stateless APIs. Symfony nutzt typischerweise das LexikJWTAuthenticationBundle.
# config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'
token_ttl: 3600 # 1 Stundenamespace 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 erzeugen
$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
{
// Bei Konfiguration vom Bundle automatisch erledigt
// Liefert ein neues Token aus dem Refresh-Token
}
}// Anpassung des JWT-Payloads
namespace App\EventListener;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
class JWTCreatedListener
{
public function onJWTCreated(JWTCreatedEvent $event): void
{
$user = $event->getUser();
$payload = $event->getData();
// Benutzerdefinierte Daten ins Token aufnehmen
$payload['user_id'] = $user->getId();
$payload['email'] = $user->getEmail();
$payload['permissions'] = $user->getPermissions();
$event->setData($payload);
}
}Das JWT-Token wird im Header Authorization: Bearer <token> übermittelt. Das Bundle prüft automatisch Signatur und Ablaufzeit.
Symfony-Formulare
Frage 11: Wie erstellt man fortgeschrittene Formulare mit Validierung?
Die Form-Komponente von Symfony erzeugt HTML-Formulare, verarbeitet die Übermittlung und validiert die Daten mit Constraints.
namespace App\Form;
use App\Entity\Article;
use App\Entity\Category;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title', TextType::class, [
'label' => 'Artikeltitel',
'attr' => ['placeholder' => 'Titel eingeben...'],
'constraints' => [
new Assert\NotBlank(message: 'Titel ist erforderlich'),
new Assert\Length(
min: 10,
max: 255,
minMessage: 'Titel muss mindestens {{ limit }} Zeichen haben',
),
],
])
->add('content', TextareaType::class, [
'label' => 'Inhalt',
'constraints' => [
new Assert\NotBlank(),
new Assert\Length(min: 100),
],
])
->add('category', EntityType::class, [
'class' => Category::class,
'choice_label' => 'name',
'placeholder' => 'Kategorie wählen',
'query_builder' => function ($repo) {
return $repo->createQueryBuilder('c')
->where('c.active = true')
->orderBy('c.name', 'ASC');
},
])
->add('coverImage', FileType::class, [
'label' => 'Titelbild',
'mapped' => false, // Nicht an die Entität gebunden
'required' => false,
'constraints' => [
new Assert\Image(
maxSize: '5M',
mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
mimeTypesMessage: 'Nicht unterstütztes Bildformat',
),
],
])
->add('publishedAt', DateTimeType::class, [
'widget' => 'single_text',
'required' => false,
'label' => 'Veröffentlichungsdatum',
]);
// Event Listener zur dynamischen Anpassung des Formulars
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$article = $event->getData();
$form = $event->getForm();
// Feld nur bei Bearbeitung hinzufügen
if ($article && $article->getId()) {
$form->add('slug', TextType::class, [
'disabled' => true,
'help' => 'Der Slug kann nicht geändert werden',
]);
}
});
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Article::class,
'validation_groups' => ['Default', 'article_creation'],
]);
}
}#[Route('/articles/new', name: 'article_new')]
public function new(Request $request, SluggerInterface $slugger): Response
{
$article = new Article();
$form = $this->createForm(ArticleType::class, $article);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Datei-Upload behandeln
$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 erfolgreich erstellt!');
return $this->redirectToRoute('article_show', ['id' => $article->getId()]);
}
return $this->render('article/new.html.twig', [
'form' => $form,
]);
}FormEvents (PRE_SET_DATA, POST_SUBMIT usw.) erlauben es, Felder dynamisch je nach Kontext anzupassen.
Frage 12: Wie implementiert man benutzerdefinierte Validierung mit Constraints?
Symfony erlaubt es, eigene Validierungs-Constraints für komplexe Geschäftsregeln zu erstellen.
// Benutzerdefinierte Constraint
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class UniqueEmail extends Constraint
{
public string $message = 'Die E-Mail "{{ value }}" wird bereits verwendet.';
public ?int $excludeId = null; // Aktuellen Benutzer beim Update ausschließen
}// Zur Constraint gehöriger Validator
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 kümmert sich um leere Werte
}
$existingUser = $this->userRepository->findOneBy(['email' => $value]);
// Prüft, ob ein Benutzer mit dieser E-Mail existiert
// und nicht der aktuelle Benutzer ist (Update-Fall)
if ($existingUser && $existingUser->getId() !== $constraint->excludeId) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $value)
->addViolation();
}
}
}// Verwendung der Constraint an der Entität
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: 'Passwort muss Groß-, Kleinbuchstaben und eine Ziffer enthalten'
)]
private ?string $plainPassword = null;
}// Klassen-Constraint für Multi-Feld-Validierung
// src/Validator/PasswordMatch.php
#[\Attribute(\Attribute::TARGET_CLASS)]
class PasswordMatch extends Constraint
{
public string $message = 'Die Passwörter stimmen nicht überein.';
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}Eigene Constraints werden automatisch erkannt. Das Suffix Validator ist für den Validator zwingend.
Messenger und asynchrone Kommunikation
Frage 13: Wie implementiert man asynchrone Verarbeitung mit Messenger?
Symfony Messenger verschickt Nachrichten in Queues zur asynchronen Verarbeitung und verbessert so die Reaktionsfähigkeit der Anwendung.
// Die Nachricht (DTO mit Daten)
namespace App\Message;
final class SendWelcomeEmail
{
public function __construct(
public readonly int $userId,
public readonly string $locale = 'en',
) {}
}// Der Handler, der die Nachricht verarbeitet
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; // Benutzer wurde inzwischen gelöscht
}
$email = (new TemplatedEmail())
->to($user->getEmail())
->subject('Willkommen auf unserer Plattform!')
->htmlTemplate('emails/welcome.html.twig')
->context([
'user' => $user,
'locale' => $message->locale,
]);
$this->mailer->send($email);
}
}# config/packages/messenger.yaml
framework:
messenger:
failure_transport: failed
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
failed:
dsn: 'doctrine://default?queue_name=failed'
routing:
# Nachrichten zum async-Transport leiten
App\Message\SendWelcomeEmail: async
App\Message\ProcessImage: async
App\Message\GenerateReport: async// Versand der Nachricht
use Symfony\Component\Messenger\MessageBusInterface;
class RegistrationController extends AbstractController
{
#[Route('/register', name: 'app_register', methods: ['POST'])]
public function register(
Request $request,
MessageBusInterface $bus,
): Response {
// ... Benutzer anlegen
// Asynchroner Versand - E-Mail wird im Hintergrund versendet
$bus->dispatch(new SendWelcomeEmail(
userId: $user->getId(),
locale: $request->getLocale(),
));
// Sofortige Antwort an den Benutzer
return $this->redirectToRoute('app_login');
}
}Der Worker wird gestartet mit php bin/console messenger:consume async -vv. In Produktion hält Supervisor den Worker am Laufen.
Frage 14: Wie behandelt man Fehler und Retries mit Messenger?
Messenger bietet robuste Mechanismen zur Fehlerbehandlung: automatischer Retry, Dead Letter Queue und manuelle Verwaltung fehlgeschlagener Nachrichten.
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) {
// Temporärer Fehler (Timeout, Rate Limit) → Retry
throw new RecoverableMessageHandlingException(
$e->getMessage(),
previous: $e
);
} catch (PaymentFailedException $e) {
// Dauerhafter Fehler (ungültige Karte) → kein Retry
throw new UnrecoverableMessageHandlingException(
$e->getMessage(),
previous: $e
);
}
}
}// Retry-Konfiguration auf Nachrichtenebene
namespace App\Message;
use Symfony\Component\Messenger\Stamp\DelayStamp;
final class ProcessPayment
{
public function __construct(
public readonly int $orderId,
public readonly int $attempt = 1,
) {}
// Benutzerdefinierte Retry-Verzögerung je nach Versuch
public function getRetryDelay(): int
{
return match ($this->attempt) {
1 => 5000, // 5 Sekunden
2 => 30000, // 30 Sekunden
3 => 300000, // 5 Minuten
default => 600000,
};
}
}# Befehle zur Verwaltung fehlgeschlagener Nachrichten
php bin/console messenger:failed:show # Fehlgeschlagene Nachrichten anzeigen
php bin/console messenger:failed:retry # Alle Nachrichten erneut verarbeiten
php bin/console messenger:failed:retry 123 # Bestimmte Nachricht erneut verarbeiten
php bin/console messenger:failed:remove 123 # Nachricht entfernenDie Retry-Strategie und der "failed"-Transport stellen sicher, dass keine Nachricht verloren geht. Nachrichten lassen sich analysieren und manuell erneut versenden.
Bereit für deine Symfony-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Tests in Symfony
Frage 15: Wie strukturiert man Tests in Symfony?
Symfony stellt PHPUnit mit dedizierten Helfern bereit, um die verschiedenen Schichten der Anwendung zu testen: Unit-, Funktions- und Integrationstests.
// Unit-Test: testet eine isolierte 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],
];
}
}// Funktionstest: testet Controller per 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
{
// Authentifizierung
$user = $this->createUser();
$this->client->loginUser($user);
// Zugriff auf das Formular
$crawler = $this->client->request('GET', '/articles/new');
$this->assertResponseIsSuccessful();
// Formular abschicken
$form = $crawler->selectButton('Erstellen')->form([
'article[title]' => 'Test Article Title',
'article[content]' => 'Dies ist der Inhalt meines Testartikels mit ausreichend Zeichen.',
]);
$this->client->submit($form);
// Verifikation
$this->assertResponseRedirects();
$this->client->followRedirect();
$this->assertSelectorTextContains('h1', 'Test Article Title');
// Datenbank-Verifikation
$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-Datenbank aufräumen
$this->entityManager->getConnection()->executeStatement('DELETE FROM article');
$this->entityManager->getConnection()->executeStatement('DELETE FROM user');
parent::tearDown();
}
}Es lohnt sich, Unit-Tests (ohne Kernel), Funktionstests (mit Kernel) und Integrationstests (mit echten Services) zu trennen.
Frage 16: Wie nutzt man Fixtures und DatabaseResetter?
Fixtures füllen die Datenbank mit realistischen Testdaten. Die Komponente DoctrineTestBundle erleichtert das Zurücksetzen zwischen Tests.
namespace App\DataFixtures;
use App\Entity\Article;
use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
class ArticleFixtures extends Fixture implements DependentFixtureInterface
{
public function load(ObjectManager $manager): void
{
for ($i = 1; $i <= 20; $i++) {
$article = new Article();
$article->setTitle("Test Article Number $i");
$article->setSlug("test-article-$i");
$article->setContent("Detaillierter Inhalt von Artikel Nummer $i...");
$article->setStatus($i <= 15 ? 'published' : 'draft');
$article->setPublishedAt($i <= 15 ? new \DateTimeImmutable("-$i days") : null);
// Referenz zum von UserFixtures erstellten User
$article->setAuthor($this->getReference('user-'.($i % 3), User::class));
$manager->persist($article);
// Referenzen für andere Fixtures anlegen
$this->addReference("article-$i", $article);
}
$manager->flush();
}
public function getDependencies(): array
{
// UserFixtures muss vor ArticleFixtures geladen werden
return [
UserFixtures::class,
];
}
}namespace App\DataFixtures;
use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class UserFixtures extends Fixture implements FixtureGroupInterface
{
public function __construct(
private readonly UserPasswordHasherInterface $passwordHasher,
) {}
public function load(ObjectManager $manager): void
{
$users = [
['email' => 'admin@example.com', 'roles' => ['ROLE_ADMIN'], 'ref' => 'user-0'],
['email' => 'author@example.com', 'roles' => ['ROLE_AUTHOR'], 'ref' => 'user-1'],
['email' => 'user@example.com', 'roles' => ['ROLE_USER'], 'ref' => 'user-2'],
];
foreach ($users as $userData) {
$user = new User();
$user->setEmail($userData['email']);
$user->setRoles($userData['roles']);
$user->setPassword($this->passwordHasher->hashPassword($user, 'password123'));
$manager->persist($user);
$this->addReference($userData['ref'], $user);
}
$manager->flush();
}
public static function getGroups(): array
{
return ['test', 'dev'];
}
}// Verwendung von DAMADoctrineTestBundle für automatisches 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); // Gemäß Fixtures
}
}# Fixtures laden
php bin/console doctrine:fixtures:load
php bin/console doctrine:fixtures:load --group=test
php bin/console doctrine:fixtures:load --env=testDAMADoctrineTestBundle umschließt jeden Test mit einer Transaktion, die zurückgerollt wird, sodass Fixtures nicht zwischen Tests neu geladen werden müssen.
Architektur und fortgeschrittene Patterns
Frage 17: Wie implementiert man CQRS mit Symfony?
CQRS (Command Query Responsibility Segregation) trennt Lese- von Schreiboperationen und ermöglicht eine unabhängige Optimierung beider.
// Command: repräsentiert eine Änderungsabsicht
namespace App\Message\Command;
final class CreateArticleCommand
{
public function __construct(
public readonly string $title,
public readonly string $content,
public readonly int $authorId,
public readonly array $tagIds = [],
) {}
}// CommandHandler: führt die Änderung aus
namespace App\MessageHandler\Command;
use App\Entity\Article;
use App\Message\Command\CreateArticleCommand;
use App\Repository\TagRepository;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\String\Slugger\SluggerInterface;
#[AsMessageHandler]
final class CreateArticleCommandHandler
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly UserRepository $userRepository,
private readonly TagRepository $tagRepository,
private readonly SluggerInterface $slugger,
) {}
public function __invoke(CreateArticleCommand $command): Article
{
$author = $this->userRepository->find($command->authorId)
?? throw new \InvalidArgumentException('Author not found');
$article = new Article();
$article->setTitle($command->title);
$article->setSlug($this->slugger->slug($command->title)->lower());
$article->setContent($command->content);
$article->setAuthor($author);
foreach ($command->tagIds as $tagId) {
$tag = $this->tagRepository->find($tagId);
if ($tag) {
$article->addTag($tag);
}
}
$this->entityManager->persist($article);
$this->entityManager->flush();
return $article;
}
}// Query: repräsentiert eine Leseanfrage
namespace App\Message\Query;
final class GetArticleBySlugQuery
{
public function __construct(
public readonly string $slug,
public readonly bool $withComments = false,
) {}
}// QueryHandler: lädt Daten (kann ein optimiertes Read-Model nutzen)
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;
}
}// Verwendung von Command- und Query-Bus
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 erlaubt es, Lesezugriffe (Caching, Projektionen) und Schreibzugriffe (Validierung, Events) getrennt zu optimieren.
Frage 18: Wie implementiert man das Repository Pattern korrekt in Symfony?
Das Repository Pattern ist in Symfony über Doctrine bereits vorhanden, lässt sich aber mit Interfaces und Geschäftsmethoden anreichern.
// Interface zur Entkopplung und Testbarkeit
namespace App\Repository\Contract;
use App\Entity\Article;
use Doctrine\Common\Collections\Collection;
interface ArticleRepositoryInterface
{
public function find(int $id): ?Article;
public function findBySlug(string $slug): ?Article;
public function findPublished(int $limit = 20, int $offset = 0): array;
public function findByAuthor(int $authorId): array;
public function save(Article $article, bool $flush = false): void;
public function remove(Article $article, bool $flush = false): void;
}// Doctrine-Implementierung des Repositories
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();
}
}
// Komplexe Query-Methoden
public function findPopularByCategory(int $categoryId, int $limit = 5): array
{
return $this->createQueryBuilder('a')
->addSelect('u')
->leftJoin('a.author', 'u')
->where('a.category = :categoryId')
->andWhere('a.status = :status')
->setParameter('categoryId', $categoryId)
->setParameter('status', 'published')
->orderBy('a.viewCount', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
}# config/services.yaml
# Bindung des Interfaces an die Implementierung
services:
App\Repository\Contract\ArticleRepositoryInterface:
alias: App\Repository\ArticleRepositoryDas Interface erlaubt Test-Implementierungen (InMemoryArticleRepository) oder einen Wechsel der Datenquelle, ohne Geschäftscode zu ändern.
Frage 19: Wie verwaltet man Konfiguration und Umgebungen in Symfony?
Symfony nutzt ein flexibles Konfigurationssystem mit Unterstützung für Umgebungsvariablen, Secrets und YAML-Dateien pro Umgebung.
# config/packages/framework.yaml
# Standardkonfiguration
framework:
secret: '%env(APP_SECRET)%'
http_method_override: false
handle_all_throwables: true
session:
handler_id: null
cookie_secure: auto
cookie_samesite: lax
storage_factory_id: session.storage.factory.native
php_errors:
log: true# config/packages/prod/framework.yaml
# Override für Produktion
framework:
session:
handler_id: '%env(REDIS_URL)%'
cookie_secure: true
when@prod:
framework:
router:
strict_requirements: null// Verwaltung sensibler Secrets (verschlüsselt)
// php bin/console secrets:set DATABASE_URL --env=prod
// php bin/console secrets:list --reveal --env=prod// Benutzerdefinierte Konfiguration für ein 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;
}
}// Konfigurations-Injection
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class ConfigurableService
{
public function __construct(
#[Autowire('%env(API_KEY)%')]
private readonly string $apiKey,
#[Autowire('%kernel.project_dir%')]
private readonly string $projectDir,
#[Autowire('%my_app.cache_ttl%')]
private readonly int $cacheTtl,
) {}
}Symfony-Secrets sind verschlüsselt und versioniert. %env(...)% sollte für Laufzeitvariablen, Parameter für statische Werte genutzt werden.
Performance und Produktion
Frage 20: Wie optimiert man die Performance einer Symfony-Anwendung?
Optimierung umfasst mehrere Ebenen: OPcache, Konfiguration, Anwendungs-Cache und Doctrine-Queries.
# Optimierte Doctrine-Konfiguration für Produktion
doctrine:
orm:
auto_generate_proxy_classes: false
metadata_cache_driver:
type: pool
pool: doctrine.system_cache_pool
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool# config/packages/cache.yaml
# Cache-Konfiguration mit 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'// Verwendung des Doctrine-Result-Caches
public function findPopularCached(): array
{
return $this->createQueryBuilder('a')
->addSelect('u')
->leftJoin('a.author', 'u')
->where('a.status = :status')
->setParameter('status', 'published')
->orderBy('a.viewCount', 'DESC')
->setMaxResults(10)
->getQuery()
->enableResultCache(3600, 'popular_articles') // Cache 1h
->getResult();
}# Optimierungsbefehle für Produktion
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 erzeugen
php bin/console doctrine:proxy:create-proxy-classes
# Optimierten Autoloader kompilieren
composer install --no-dev --optimize-autoloader --classmap-authoritativeOPcache muss in Produktion mit optimalen Einstellungen aktiv sein. Der Warmup erzeugt den Container- und Router-Cache.
Frage 21: Wie konfiguriert man Logging und Monitoring in Symfony?
Strukturiertes Logging und passendes Monitoring sind essenziell, um Probleme in Produktion zu diagnostizieren.
# 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'// Strukturiertes Logging mit Kontext
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;
}
}
}// Logging von 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),
]);
}
}Das JSON-Format erleichtert die Aufnahme in Monitoring-Tools (ELK, Datadog). Kanäle filtern nach Log-Typ.
Frage 22: Wie deployt man eine Symfony-Anwendung in Produktion?
Ein robustes Symfony-Deployment kombiniert Build-Vorbereitung, sichere Migrationen und atomares Umschalten.
#!/bin/bash
# deploy.sh - Deployment-Skript
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 "Erzeuge Release-Verzeichnis..."
mkdir -p $RELEASE_DIR
echo "Klone Repository..."
git clone --depth 1 --branch main git@github.com:org/repo.git $RELEASE_DIR
echo "Installiere Abhängigkeiten..."
cd $RELEASE_DIR
composer install --no-dev --optimize-autoloader --classmap-authoritative
echo "Verlinke gemeinsame Dateien..."
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 "Führe Migrationen aus..."
php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
echo "Wärme Cache auf..."
php bin/console cache:clear --env=prod --no-debug
php bin/console cache:warmup --env=prod --no-debug
echo "Setze Berechtigungen..."
chown -R www-data:www-data $RELEASE_DIR
echo "Schalte auf neue Release um..."
ln -sfn $RELEASE_DIR $CURRENT_LINK
echo "Starte PHP-FPM neu..."
sudo systemctl reload php8.3-fpm
echo "Starte Messenger-Worker neu..."
php bin/console messenger:stop-workers
echo "Räume alte Releases auf..."
ls -dt /var/www/releases/*/ | tail -n +6 | xargs rm -rf
echo "Deployment abgeschlossen!"# .github/workflows/deploy.yml
# CI/CD-Deployment mit 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.shAtomares Deployment per Symlink erlaubt sofortigen Rollback. Messenger-Worker müssen neu gestartet werden, um den neuen Code zu laden.
API Platform und REST
Frage 23: Wie baut man eine REST-API mit API Platform?
API Platform ist die Standardlösung zum Erstellen von REST- und GraphQL-APIs mit Symfony und liefert Auto-Dokumentation und HTTP-Standards.
// API-Platform-Resource-Konfiguration
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;
}// Benutzerdefinierter Processor für Geschäftslogik
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()) {
// Neuer Artikel: Autor und Slug setzen
$data->setAuthor($this->security->getUser());
$data->setSlug($this->slugger->slug($data->getTitle())->lower());
}
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
}
}# config/packages/api_platform.yaml
api_platform:
title: 'My API'
version: '1.0.0'
formats:
jsonld: ['application/ld+json']
json: ['application/json']
docs_formats:
jsonld: ['application/ld+json']
jsonopenapi: ['application/vnd.openapi+json']
html: ['text/html']
defaults:
pagination_items_per_page: 20
pagination_client_items_per_page: true
pagination_maximum_items_per_page: 100
swagger:
versions: [3]API Platform erzeugt automatisch die OpenAPI-Dokumentation und liefert Filter, Pagination und Validierung.
Frage 24: Wie passt man API-Platform-Operationen an?
API Platform erlaubt es, eigene Operationen mit Controllern oder State Providern/Processoren zu erstellen.
// Benutzerdefinierte Operationen
#[ApiResource(
operations: [
// Standardoperationen
new GetCollection(),
new Get(),
// Eigene Operation mit Controller
new Post(
uriTemplate: '/articles/{id}/publish',
controller: PublishArticleController::class,
openapi: new Model\Operation(
summary: 'Veröffentlicht einen Artikel',
description: 'Setzt den Artikel-Status auf "published"',
),
security: "is_granted('ARTICLE_EDIT', object)",
),
// Operation mit eigenem State Provider
new GetCollection(
uriTemplate: '/articles/trending',
provider: TrendingArticlesProvider::class,
openapiContext: ['summary' => 'Trending-Artikel'],
),
],
)]
class Article
{
// ...
}// Controller für eigene Operation
namespace App\Controller;
use App\Entity\Article;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\AsController;
#[AsController]
class PublishArticleController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
) {}
public function __invoke(Article $article): Article
{
if ($article->getStatus() === 'published') {
throw $this->createNotFoundException('Artikel bereits veröffentlicht');
}
$article->setStatus('published');
$article->setPublishedAt(new \DateTimeImmutable());
$this->entityManager->flush();
return $article;
}
}// State Provider für eigene Lese-Logik
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
{
// Eigene Logik für Trending-Artikel
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 Provider übernehmen das Lesen, State Processor das Schreiben. Controller bleiben für komplexe Fälle verfügbar.
Frage 25: Wie verwaltet man Datenbank-Migrationen in Produktion?
Doctrine-Migrationen müssen so gestaltet sein, dass sie ohne Downtime laufen und einfaches Rollback ermöglichen.
// Sichere Migration: Spalte nullable hinzufügen
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
{
// Schritt 1: nullable-Spalte hinzufügen
$this->addSql('ALTER TABLE users ADD role VARCHAR(50) DEFAULT NULL');
// Index CONCURRENTLY anlegen (PostgreSQL - nicht blockierend)
$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');
}
}// Datenmigration (separat)
final class Version20260202100001 extends AbstractMigration
{
public function getDescription(): string
{
return 'Populate role column with default value';
}
public function up(Schema $schema): void
{
// Batch-Migration für große Tabellen
$this->addSql("UPDATE users SET role = 'ROLE_USER' WHERE role IS NULL");
}
public function down(Schema $schema): void
{
// Kein Rollback für Daten nötig
}
}// Letzte Migration: Spalte auf NOT NULL setzen
final class Version20260202100002 extends AbstractMigration
{
public function getDescription(): string
{
return 'Make role column NOT NULL';
}
public function up(Schema $schema): void
{
// Vorprüfung
$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');
}
}# Befehle zur Migrationsverwaltung
php bin/console doctrine:migrations:status # Migrations-Status
php bin/console doctrine:migrations:migrate # Migrationen ausführen
php bin/console doctrine:migrations:migrate prev # Letzte Migration zurückrollen
php bin/console doctrine:migrations:diff # Migration aus Schema generieren
php bin/console doctrine:migrations:execute --down # Konkretes RollbackDie Expand-Contract-Strategie (3 Deployments) gewährleistet Zero-Downtime: nullable hinzufügen → Daten migrieren → Constraint anlegen.
Fazit
Diese 25 Fragen decken die wesentlichen Themen von Symfony-Interviews ab — von Service-Container-Grundlagen bis zu fortgeschrittenen Produktionspatterns.
Vorbereitungs-Checkliste:
- ✅ Service Container und Dependency Injection
- ✅ Doctrine ORM: Beziehungen, Queries, Filter
- ✅ Security: Authentifizierung, Voter, JWT
- ✅ Formulare und benutzerdefinierte Validierung
- ✅ Messenger: Asynchrone Verarbeitung und Fehlerbehandlung
- ✅ Tests: Unit, Funktional, Fixtures
- ✅ Architektur: CQRS, Repository Pattern
- ✅ API Platform: REST, eigene Operationen
- ✅ Produktion: Performance, Logging, Deployment
Jede Frage verdient eine tiefere Beschäftigung mit der offiziellen Symfony-Dokumentation. Recruiter schätzen Kandidaten, die die architektonischen Entscheidungen des Frameworks verstehen und ihre technischen Entscheidungen begründen können.
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Tags
Teilen
Verwandte Artikel

Doctrine ORM: Beziehungen in Symfony meistern
Vollständiger Leitfaden zu Doctrine-ORM-Beziehungen in Symfony. OneToMany, ManyToMany, Ladestrategien und Performance-Optimierung mit praktischen Beispielen.

Symfony 7: API Platform und Best Practices
Vollstaendiger Leitfaden zu API Platform 4 mit Symfony 7. State Processors, State Providers, Serialisierungsgruppen und erweiterte Validierung fuer professionelle REST-APIs.

Symfony Live Components und UX 3.0: Reaktive Anwendungen ohne JavaScript im Jahr 2026
Dieses Tutorial zeigt, wie sich mit Symfony Live Components und UX 3.0 reaktive Oberflächen ganz ohne JavaScript erstellen lassen. Mit praktischen Codebeispielen für Suche, Warenkorb und Formulare.