Symfony Mülakat Soruları: 2026 İlk 25
En sık sorulan 25 Symfony mülakat sorusu. Mimari, Doctrine ORM, servisler, güvenlik, formlar ve testler ayrıntılı yanıtlar ve kod örnekleriyle.

Symfony mülakatları, profesyonel referans PHP framework'ünün ustalığını, bileşen odaklı mimari, Doctrine ORM anlayışını ve sağlam, ölçeklenebilir uygulamalar geliştirme yeteneğini ölçer. Bu rehber, Symfony temellerinden ileri seviye üretim örüntülerine kadar en sık sorulan 25 soruyu kapsar.
İşe alımcılar Symfony felsefesini kavrayan adayları takdir eder: servisler aracılığıyla ayrıştırma, açık yapılandırma ve PSR standartlarına uyum. Framework'ün mimari tercihlerini açıklayabilmek farkı yaratır.
Symfony temelleri
Soru 1: Symfony'de bir isteğin yaşam döngüsünü açıklayın
Symfony isteğinin yaşam döngüsü HTTP Kernel'i geçer ve her adımda genişletmeye olanak sağlamak için bir olay sistemi kullanır. Bu döngüyü anlamak hata ayıklama ve uygulama davranışını özelleştirme için kritiktir.
// Tüm HTTP istekleri için giriş noktası
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
// Symfony Runtime bootstrap'i yönetir
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};// Kernel istek işleme sürecini orkestre eder
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
// Kernel bundle'ları yükler ve container'ı yapılandırır
// Yaşam döngüsünün anahtar olayları:
// 1. kernel.request - Routing'den önce
// 2. kernel.controller - Controller çözümlendikten sonra
// 3. kernel.view - Controller bir Response döndürmediyse
// 4. kernel.response - Yanıt gönderilmeden önce
// 5. kernel.terminate - Gönderildikten sonra (asenkron görevler)
}Tam döngü: index.php → Runtime → Kernel → HttpKernel → EventDispatcher (kernel.request) → Router → Controller → EventDispatcher (kernel.response) → Response. Her adım Event Subscriber'lar ile araya alınabilir.
Soru 2: Symfony'de Service Container ve Dependency Injection nedir?
Service Container (yani DIC, Dependency Injection Container) Symfony'nin kalbidir. Uygulamanın tüm servislerinin örneklenmesini, yapılandırılmasını ve enjeksiyonunu yönetir.
// Otomatik enjekte edilen bağımlılıklara sahip servis
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, // Yapılandırılmış HTTP istemcisi
private readonly OrderRepository $orderRepository, // Doctrine repository
private readonly LoggerInterface $logger, // PSR-3 logger
private readonly string $stripeApiKey, // Enjekte edilmiş parametre
) {}
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
# Servis yapılandırması
services:
_defaults:
autowire: true # Type-hint ile otomatik enjeksiyon
autoconfigure: true # Otomatik tag yapılandırması
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# Parametrelerle açık yapılandırma
App\Service\PaymentService:
arguments:
$stripeClient: '@stripe.client'
$stripeApiKey: '%env(STRIPE_API_KEY)%'Autowiring, bağımlılıkları type-hint ile otomatik olarak çözer. Skaler parametreler açık yapılandırma gerektirir.
Soru 3: Bundle ile Symfony bileşeni arasındaki fark nedir?
Bundle'lar, bir Symfony uygulamasına işlevler entegre eden yeniden kullanılabilir paketlerdir. Bileşenler, Symfony olmadan kullanılabilen bağımsız PHP kütüphaneleridir.
// Özel bir Bundle yapısı
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
{
// Bundle yapılandırmasını yükler
public function loadExtension(
array $config,
ContainerConfigurator $container,
ContainerBuilder $builder
): void {
// Bundle servislerini yükler
$container->import('../config/services.yaml');
// Koşullu yapılandırma
if ($config['feature_enabled']) {
$container->services()
->set('my_bundle.feature_service', FeatureService::class)
->autowire();
}
}
// Bundle varsayılan yapılandırması
public function configure(DefinitionConfigurator $definition): void
{
$definition->rootNode()
->children()
->booleanNode('feature_enabled')->defaultTrue()->end()
->scalarNode('api_key')->isRequired()->end()
->end();
}
}// Symfony olmadan bir bileşen kullanımı
// Bileşenler bağımsız PHP kütüphaneleridir
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// Herhangi bir PHP projesinde kullanılabilir
$request = Request::createFromGlobals();
$response = new Response('Hello World', 200);
$response->send();Bundle'lar yapılandırma, servis ve kaynakları kapsüller. Bileşenler her yerde yeniden kullanılabilen düşük seviyeli araçlardır.
Soru 4: Symfony'de Event Subscriber'lar nasıl çalışır?
Event Subscriber'lar framework veya uygulama olaylarına tepki vermeyi sağlar ve iş mantığını ana koddan ayrıştırır.
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
{
// Dinlenen olayları ve önceliklerini bildirir
public static function getSubscribedEvents(): array
{
return [
// Yüksek öncelik (diğerlerinden önce çalıştırılır)
KernelEvents::EXCEPTION => ['onKernelException', 100],
KernelEvents::RESPONSE => ['onKernelResponse', 0],
];
}
public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
$request = $event->getRequest();
// Yalnızca API isteklerini işler
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);
// Yanıtı kendi JSON'umuzla değiştirir
$event->setResponse($response);
}
public function onKernelResponse(\Symfony\Component\HttpKernel\Event\ResponseEvent $event): void
{
// Özel başlıklar ekler
$event->getResponse()->headers->set('X-Api-Version', '1.0');
}
}Event Subscriber'lar autoconfigure sayesinde otomatik olarak keşfedilir. Öncelik çalıştırma sırasını belirler (yüksek = önce çalıştırılır).
Doctrine ORM
Soru 5: Doctrine ilişkilerini ve farklarını açıklayın
Doctrine, varlıklar arasındaki ilişkileri modellemek için çeşitli ilişki türleri sunar. Her tür sorgu ve performans üzerinde etkilere sahiptir.
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 ilişkisi: bir kullanıcının bir profili vardır
#[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])]
private ?Profile $profile = null;
// OneToMany ilişkisi: bir kullanıcının birden çok makalesi vardır
#[ORM\OneToMany(mappedBy: 'author', targetEntity: Article::class, orphanRemoval: true)]
private Collection $articles;
// ManyToMany ilişkisi: birden çok kullanıcının birden çok rolü vardır
#[ORM\ManyToMany(targetEntity: Role::class, inversedBy: 'users')]
#[ORM\JoinTable(name: 'user_roles')]
private Collection $roles;
public function __construct()
{
// Koleksiyonun zorunlu başlatılması
$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); // Çift yönlü senkronizasyon
}
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 ilişkisi: birden çok makale bir yazara aittir
#[ORM\ManyToOne(inversedBy: 'articles')]
#[ORM\JoinColumn(nullable: false)]
private ?User $author = null;
// Pivot varlık üzerinden ek özelliklerle ManyToMany
#[ORM\OneToMany(mappedBy: 'article', targetEntity: ArticleTag::class)]
private Collection $articleTags;
}Çift yönlü ilişkiler manuel senkronizasyon gerektirir. "Owning" tarafı (JoinColumn/JoinTable ile) kalıcılığı kontrol eder.
Soru 6: N+1 sorunu nedir ve Doctrine ile nasıl çözülür?
N+1 sorunu, ana bir sorgunun ilişkileri yüklemek için N ek sorgu oluşturduğunda meydana gelir. Symfony uygulamalarında yavaşlığın en yaygın sebebidir.
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);
}
// KÖTÜ: yazarlara erişilirse N+1 sorgu
public function findAllBad(): array
{
return $this->findAll();
// + her makale için yazarı yüklemek için 1 sorgu
}
// İYİ: eager fetch ile JOIN
public function findAllWithAuthor(): array
{
return $this->createQueryBuilder('a')
->addSelect('u') // Yazarı da SELECT et
->leftJoin('a.author', 'u') // İlişki üzerinden JOIN
->orderBy('a.publishedAt', 'DESC')
->getQuery()
->getResult();
}
// İYİ: birden fazla ilişki için birden fazla JOIN
public function findAllWithDetails(): array
{
return $this->createQueryBuilder('a')
->addSelect('u', 'c', 't') // Tüm ilişkileri SELECT et
->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();
}
// İYİ: büyük listeler için toplu yükleme
public function findAllOptimized(): array
{
$query = $this->createQueryBuilder('a')
->getQuery();
// İlişkileri 100'lük batch'lerde yükler
$query->setFetchMode(Article::class, 'author', ClassMetadata::FETCH_BATCH);
return $query->getResult();
}
}Doctrine paneliyle Symfony Profiler N+1 sorunlarını tespit etmeyi sağlar. Sorgu sayısı Web Debug Toolbar'da görünür.
Soru 7: Query Extension ve Doctrine filtreleri nasıl oluşturulur?
Query Extension'lar ve Doctrine filtreleri tüm sorgulara koşulları otomatik olarak uygulamaya yarar — multi-tenancy veya soft delete için idealdir.
// Otomatik olarak kullanıcıya göre filtrelemek için API Platform extension
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
{
// Yalnızca Article'a uygulanır
if ($resourceClass !== Article::class) {
return;
}
// Admin her şeyi görür
if ($this->security->isGranted('ROLE_ADMIN')) {
return;
}
$user = $this->security->getUser();
$rootAlias = $queryBuilder->getRootAliases()[0];
// Yazara göre otomatik filtre
$queryBuilder
->andWhere(sprintf('%s.author = :current_user', $rootAlias))
->setParameter('current_user', $user);
}
}// Silinmiş öğeleri hariç tutmak için global Doctrine filtresi
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
{
// Varlığın deletedAt alanı olup olmadığını kontrol eder
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: trueFiltreler SQL düzeyinde, extension'lar QueryBuilder düzeyinde uygulanır. Geçici olarak $em->getFilters()->disable('soft_delete') ile devre dışı bırakılabilir.
Symfony mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
Symfony güvenliği
Soru 8: Symfony'nin güvenlik sistemi nasıl çalışır?
Symfony'nin Security bileşeni, kimlik doğrulamayı (kullanıcı kimdir) ve yetkilendirmeyi (ne yapabilir) genişletilebilir bir mimari ile yönetir.
# 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 }// Özel mantık için özel authenticator
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
{
// Bu authenticator yalnızca X-API-KEY içeren istekleri işler
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 kullanıcıyı identifier'a göre yükler
return new SelfValidatingPassport(
new UserBadge($apiKey, function (string $apiKey) {
// API anahtarı ile kullanıcı yükleme mantığı
return $this->userRepository->findByApiKey($apiKey);
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// null = isteğe normal şekilde devam et
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new JsonResponse([
'error' => $exception->getMessage(),
], Response::HTTP_UNAUTHORIZED);
}
}Güvenlik mimarisi Firewall'lar (yapılandırma), Authenticator'lar (kimlik doğrulama) ve Voter'lar (yetkilendirme) üzerine kuruludur.
Soru 9: İnce taneli yetkilendirme için Voter'lar nasıl uygulanır?
Voter'lar karmaşık ve yeniden kullanılabilir yetkilendirme mantığına olanak sağlar; iş kurallarını controller kodundan ayırır.
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
{
// Bu voter yalnızca Article ve bu öznitelikleri işler
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;
// Yayımlanmış makaleler herkese görünür
if ($attribute === self::VIEW && $article->isPublished()) {
return true;
}
// Diğer eylemler kimliği doğrulanmış kullanıcı gerektirir
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
{
// Taslaklar yalnızca yazara veya adminlere görünür
return $article->getAuthor() === $user
|| in_array('ROLE_ADMIN', $user->getRoles());
}
private function canEdit(Article $article, User $user): bool
{
// Yalnızca yazar düzenleyebilir
return $article->getAuthor() === $user;
}
private function canDelete(Article $article, User $user): bool
{
// Yazar veya admin silebilir
return $article->getAuthor() === $user
|| in_array('ROLE_ADMIN', $user->getRoles());
}
}// Bir controller'da Voter kullanımı
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
{
// Yetkilendirme otomatik olarak kontrol edilir
// Voter erişimi reddederse 403
}
// Programatik alternatif
#[Route('/articles/{id}', name: 'article_show')]
public function show(Article $article): Response
{
$this->denyAccessUnlessGranted(ArticleVoter::VIEW, $article);
// Veya koşullu
if (!$this->isGranted(ArticleVoter::EDIT, $article)) {
// Düzenleme düğmesini gizle
}
}
}{# Twig içinde #}
{% if is_granted('ARTICLE_EDIT', article) %}
<a href="{{ path('article_edit', {id: article.id}) }}">Düzenle</a>
{% endif %}Voter'lar otomatik olarak keşfedilir ve isGranted() çağrılarında danışılır. Varsayılan strateji en az bir Voter olumlu oy verirse erişim verir.
Soru 10: Symfony'de bir API JWT ile nasıl korunur?
JWT (JSON Web Token) kimlik doğrulaması, durumsuz API'ler için standart çözümdür. Symfony genellikle LexikJWTAuthenticationBundle'ı kullanır.
# 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 saatnamespace 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 üret
$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
{
// Yapılandırılmışsa bundle tarafından otomatik yönetilir
// Refresh token'dan yeni token döndürür
}
}// JWT payload'ının özelleştirilmesi
namespace App\EventListener;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
class JWTCreatedListener
{
public function onJWTCreated(JWTCreatedEvent $event): void
{
$user = $event->getUser();
$payload = $event->getData();
// Token'a özel veri ekler
$payload['user_id'] = $user->getId();
$payload['email'] = $user->getEmail();
$payload['permissions'] = $user->getPermissions();
$event->setData($payload);
}
}JWT token, Authorization: Bearer <token> başlığında gönderilir. Bundle imzayı ve süresini otomatik olarak doğrular.
Symfony formları
Soru 11: Doğrulama ile gelişmiş formlar nasıl oluşturulur?
Symfony'nin Form bileşeni HTML formları üretir, gönderimi yönetir ve verileri kısıtlamalarla doğrular.
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' => 'Makale başlığı',
'attr' => ['placeholder' => 'Başlığı girin...'],
'constraints' => [
new Assert\NotBlank(message: 'Başlık zorunludur'),
new Assert\Length(
min: 10,
max: 255,
minMessage: 'Başlık en az {{ limit }} karakter olmalıdır',
),
],
])
->add('content', TextareaType::class, [
'label' => 'İçerik',
'constraints' => [
new Assert\NotBlank(),
new Assert\Length(min: 100),
],
])
->add('category', EntityType::class, [
'class' => Category::class,
'choice_label' => 'name',
'placeholder' => 'Kategori seçin',
'query_builder' => function ($repo) {
return $repo->createQueryBuilder('c')
->where('c.active = true')
->orderBy('c.name', 'ASC');
},
])
->add('coverImage', FileType::class, [
'label' => 'Kapak görseli',
'mapped' => false, // Varlığa bağlı değil
'required' => false,
'constraints' => [
new Assert\Image(
maxSize: '5M',
mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
mimeTypesMessage: 'Desteklenmeyen görsel formatı',
),
],
])
->add('publishedAt', DateTimeType::class, [
'widget' => 'single_text',
'required' => false,
'label' => 'Yayın tarihi',
]);
// Formu dinamik olarak değiştirmek için event listener
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$article = $event->getData();
$form = $event->getForm();
// Alanı yalnızca düzenleme sırasında ekle
if ($article && $article->getId()) {
$form->add('slug', TextType::class, [
'disabled' => true,
'help' => 'Slug değiştirilemez',
]);
}
});
}
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()) {
// Dosya yükleme yönetimi
$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', 'Makale başarıyla oluşturuldu!');
return $this->redirectToRoute('article_show', ['id' => $article->getId()]);
}
return $this->render('article/new.html.twig', [
'form' => $form,
]);
}FormEvents (PRE_SET_DATA, POST_SUBMIT vb.) bağlama göre alanları dinamik olarak değiştirmeyi sağlar.
Soru 12: Kısıtlamalarla özel doğrulama nasıl uygulanır?
Symfony, karmaşık iş kuralları için özel doğrulama kısıtlamaları oluşturmaya olanak sağlar.
// Özel kısıtlama
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class UniqueEmail extends Constraint
{
public string $message = '"{{ value }}" e-postası zaten kullanılıyor.';
public ?int $excludeId = null; // Güncellemede mevcut kullanıcıyı hariç tutmak için
}// Kısıtlamaya bağlı 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 boş değerleri yönetir
}
$existingUser = $this->userRepository->findOneBy(['email' => $value]);
// Bu e-postaya sahip bir kullanıcının var olup olmadığını
// ve mevcut kullanıcı olmadığını kontrol et (güncelleme durumunda)
if ($existingUser && $existingUser->getId() !== $constraint->excludeId) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $value)
->addViolation();
}
}
}// Kısıtlamayı varlıkta kullanma
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: 'Parola büyük, küçük harf ve rakam içermelidir'
)]
private ?string $plainPassword = null;
}// Çok alanlı doğrulama için sınıf düzeyinde kısıtlama
// src/Validator/PasswordMatch.php
#[\Attribute(\Attribute::TARGET_CLASS)]
class PasswordMatch extends Constraint
{
public string $message = 'Parolalar eşleşmiyor.';
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}Özel kısıtlamalar otomatik olarak keşfedilir. Validator soneki validator için zorunludur.
Messenger ve asenkron iletişim
Soru 13: Messenger ile asenkron işleme nasıl uygulanır?
Symfony Messenger, mesajları asenkron işlenmek üzere kuyruklara gönderir; bu da uygulamanın yanıt verme hızını artırır.
// Mesaj (verileri içeren DTO)
namespace App\Message;
final class SendWelcomeEmail
{
public function __construct(
public readonly int $userId,
public readonly string $locale = 'en',
) {}
}// Mesajı işleyen handler
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; // Kullanıcı bu sırada silinmiş
}
$email = (new TemplatedEmail())
->to($user->getEmail())
->subject('Platformumuza hoş geldiniz!')
->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:
# Mesajları async transport'a yönlendir
App\Message\SendWelcomeEmail: async
App\Message\ProcessImage: async
App\Message\GenerateReport: async// Mesaj gönderimi
use Symfony\Component\Messenger\MessageBusInterface;
class RegistrationController extends AbstractController
{
#[Route('/register', name: 'app_register', methods: ['POST'])]
public function register(
Request $request,
MessageBusInterface $bus,
): Response {
// ... kullanıcı oluşturma
// Asenkron gönderim - e-posta arka planda gönderilecek
$bus->dispatch(new SendWelcomeEmail(
userId: $user->getId(),
locale: $request->getLocale(),
));
// Kullanıcıya hemen yanıt
return $this->redirectToRoute('app_login');
}
}Worker php bin/console messenger:consume async -vv ile başlatılır. Üretimde worker'ı çalışır durumda tutmak için Supervisor kullanılır.
Soru 14: Messenger ile hatalar ve yeniden deneme nasıl yönetilir?
Messenger arızaları yönetmek için sağlam mekanizmalar sunar: otomatik yeniden deneme, dead letter queue ve başarısız mesajları manuel yönetme.
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) {
// Geçici hata (timeout, rate limit) → yeniden dene
throw new RecoverableMessageHandlingException(
$e->getMessage(),
previous: $e
);
} catch (PaymentFailedException $e) {
// Kalıcı hata (geçersiz kart) → yeniden denemeden
throw new UnrecoverableMessageHandlingException(
$e->getMessage(),
previous: $e
);
}
}
}// Mesaj düzeyinde retry yapılandırması
namespace App\Message;
use Symfony\Component\Messenger\Stamp\DelayStamp;
final class ProcessPayment
{
public function __construct(
public readonly int $orderId,
public readonly int $attempt = 1,
) {}
// Deneme sayısına göre özel retry gecikmesi
public function getRetryDelay(): int
{
return match ($this->attempt) {
1 => 5000, // 5 saniye
2 => 30000, // 30 saniye
3 => 300000, // 5 dakika
default => 600000,
};
}
}# Başarısız mesaj yönetimi komutları
php bin/console messenger:failed:show # Başarısız mesajları listele
php bin/console messenger:failed:retry # Tüm mesajları yeniden dene
php bin/console messenger:failed:retry 123 # Belirli mesajı yeniden dene
php bin/console messenger:failed:remove 123 # Bir mesajı kaldırRetry stratejisi ve "failed" transport hiçbir mesajın kaybolmamasını sağlar. Mesajlar analiz edilip manuel olarak yeniden denenebilir.
Symfony mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
Symfony'de testler
Soru 15: Symfony'de testler nasıl yapılandırılır?
Symfony, uygulamanın farklı katmanlarını test etmek için adanmış helper'larla PHPUnit sağlar: birim, fonksiyonel ve entegrasyon testleri.
// Birim test: izole bir sınıfı test eder
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],
];
}
}// Fonksiyonel test: controller'ları HTTP üzerinden test eder
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
{
// Kimlik doğrulama
$user = $this->createUser();
$this->client->loginUser($user);
// Forma erişim
$crawler = $this->client->request('GET', '/articles/new');
$this->assertResponseIsSuccessful();
// Form gönderimi
$form = $crawler->selectButton('Oluştur')->form([
'article[title]' => 'Test Article Title',
'article[content]' => 'Bu, yeterli karakterli test makalemin içeriğidir.',
]);
$this->client->submit($form);
// Doğrulama
$this->assertResponseRedirects();
$this->client->followRedirect();
$this->assertSelectorTextContains('h1', 'Test Article Title');
// Veritabanı doğrulaması
$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 veritabanını temizle
$this->entityManager->getConnection()->executeStatement('DELETE FROM article');
$this->entityManager->getConnection()->executeStatement('DELETE FROM user');
parent::tearDown();
}
}Birim testler (kernel'siz), fonksiyonel testler (kernel'li) ve entegrasyon testleri (gerçek servislerle) ayırmak doğrudur.
Soru 16: Fixture'lar ve DatabaseResetter nasıl kullanılır?
Fixture'lar veritabanını gerçekçi test verileriyle doldurur. DoctrineTestBundle bileşeni testler arasında reset'i kolaylaştırır.
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("Numara $i makalesinin ayrıntılı içeriği...");
$article->setStatus($i <= 15 ? 'published' : 'draft');
$article->setPublishedAt($i <= 15 ? new \DateTimeImmutable("-$i days") : null);
// UserFixtures tarafından oluşturulan kullanıcıya referans
$article->setAuthor($this->getReference('user-'.($i % 3), User::class));
$manager->persist($article);
// Diğer fixture'lar için referans oluştur
$this->addReference("article-$i", $article);
}
$manager->flush();
}
public function getDependencies(): array
{
// UserFixtures, ArticleFixtures'tan önce yüklenmeli
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'];
}
}// Otomatik reset için DAMADoctrineTestBundle kullanımı
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); // Fixture'lara göre
}
}# Fixture'ları yükleme
php bin/console doctrine:fixtures:load
php bin/console doctrine:fixtures:load --group=test
php bin/console doctrine:fixtures:load --env=testDAMADoctrineTestBundle her testi geri alınan bir transaksiyon içine sarar; böylece testler arası fixture yeniden yüklemekten kaçınılır.
Mimari ve ileri seviye örüntüler
Soru 17: Symfony ile CQRS nasıl uygulanır?
CQRS (Command Query Responsibility Segregation) okuma işlemlerini yazma işlemlerinden ayırır ve bağımsız optimizasyon yapılmasına olanak sağlar.
// Command: bir değişiklik niyetini temsil eder
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: değişikliği uygular
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: bir okuma talebini temsil eder
namespace App\Message\Query;
final class GetArticleBySlugQuery
{
public function __construct(
public readonly string $slug,
public readonly bool $withComments = false,
) {}
}// QueryHandler: verileri alır (optimize bir read model kullanabilir)
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;
}
}// Command ve Query Bus kullanımı
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, okumaları (caching, projeksiyonlar) ve yazmaları (doğrulama, olaylar) ayrı ayrı optimize etmeye olanak sağlar.
Soru 18: Symfony'de Repository Pattern nasıl doğru uygulanır?
Repository Pattern Symfony'de Doctrine üzerinden zaten mevcuttur ancak arayüzler ve iş metodları ile zenginleştirilebilir.
// Ayrıştırma ve test edilebilirlik için arayüz
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;
}// Repository'nin Doctrine implementasyonu
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();
}
}
// Karmaşık sorgu metodları
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
# Arayüzü implementasyonla bağlama
services:
App\Repository\Contract\ArticleRepositoryInterface:
alias: App\Repository\ArticleRepositoryArayüz, test implementasyonları (InMemoryArticleRepository) oluşturmayı veya iş kodunu değiştirmeden veri kaynağını değiştirmeyi mümkün kılar.
Soru 19: Symfony'de yapılandırma ve ortamlar nasıl yönetilir?
Symfony, ortam değişkenleri, secret'lar ve ortam başına YAML dosyaları desteğiyle esnek bir yapılandırma sistemi kullanır.
# config/packages/framework.yaml
# Varsayılan yapılandırma
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
# Üretim için override
framework:
session:
handler_id: '%env(REDIS_URL)%'
cookie_secure: true
when@prod:
framework:
router:
strict_requirements: null// Hassas (şifrelenmiş) secret'ların yönetimi
// php bin/console secrets:set DATABASE_URL --env=prod
// php bin/console secrets:list --reveal --env=prod// Bir bundle için özel yapılandırma
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;
}
}// Yapılandırma enjeksiyonu
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 secret'ları şifrelenir ve sürümlenir. Çalışma zamanı değişkenleri için %env(...)%, statik değerler için parametreler kullanılmalıdır.
Performans ve üretim
Soru 20: Bir Symfony uygulamasının performansı nasıl optimize edilir?
Optimizasyon birkaç düzeyi kapsar: opcache, yapılandırma, uygulama önbelleği ve Doctrine sorguları.
# Üretim için optimize edilmiş Doctrine yapılandırması
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
# Redis ile önbellek yapılandırması
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'// Doctrine sonuç önbelleği kullanımı
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') // 1 saat önbellek
->getResult();
}# Üretim için optimizasyon komutları
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 proxy'lerini oluştur
php bin/console doctrine:proxy:create-proxy-classes
# Optimize autoloader derlemesi
composer install --no-dev --optimize-autoloader --classmap-authoritativeOPcache üretimde optimal ayarlarla etkin olmalıdır. Warmup, container ve router önbelleğini oluşturur.
Soru 21: Symfony'de loglama ve izleme nasıl yapılandırılır?
Yapılandırılmış loglama ve uygun izleme, üretimdeki sorunları teşhis etmek için temeldir.
# 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'// Bağlamla yapılandırılmış loglama
namespace App\Service;
use Psr\Log\LoggerInterface;
class PaymentService
{
public function __construct(
private readonly LoggerInterface $logger,
) {}
public function processPayment(Order $order): bool
{
$this->logger->info('Payment processing started', [
'order_id' => $order->getId(),
'amount' => $order->getTotal(),
'currency' => $order->getCurrency(),
'user_id' => $order->getUser()->getId(),
]);
try {
$result = $this->gateway->charge($order);
$this->logger->info('Payment successful', [
'order_id' => $order->getId(),
'transaction_id' => $result->getTransactionId(),
]);
return true;
} catch (\Exception $e) {
$this->logger->error('Payment failed', [
'order_id' => $order->getId(),
'error' => $e->getMessage(),
'exception' => $e,
]);
return false;
}
}
}// HTTP istek loglaması
namespace App\EventSubscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class RequestLoggerSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly LoggerInterface $logger,
) {}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::TERMINATE => 'onKernelTerminate',
];
}
public function onKernelTerminate(TerminateEvent $event): void
{
$request = $event->getRequest();
$response = $event->getResponse();
$this->logger->info('Request completed', [
'method' => $request->getMethod(),
'uri' => $request->getRequestUri(),
'status' => $response->getStatusCode(),
'duration_ms' => round((microtime(true) - $request->server->get('REQUEST_TIME_FLOAT')) * 1000),
'memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
]);
}
}JSON formatı izleme araçları (ELK, Datadog) tarafından alınmasını kolaylaştırır. Channel'lar log türüne göre filtrelemeye olanak sağlar.
Soru 22: Symfony uygulaması üretime nasıl deploy edilir?
Sağlam bir Symfony deploy'u, build hazırlığı, güvenli migration'lar ve atomik geçişi birleştirir.
#!/bin/bash
# deploy.sh - Deploy scripti
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 dizini oluşturuluyor..."
mkdir -p $RELEASE_DIR
echo "Repository klonlanıyor..."
git clone --depth 1 --branch main git@github.com:org/repo.git $RELEASE_DIR
echo "Bağımlılıklar yükleniyor..."
cd $RELEASE_DIR
composer install --no-dev --optimize-autoloader --classmap-authoritative
echo "Paylaşılan dosyalar bağlanıyor..."
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 "Migration'lar çalıştırılıyor..."
php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
echo "Önbellek ısıtılıyor..."
php bin/console cache:clear --env=prod --no-debug
php bin/console cache:warmup --env=prod --no-debug
echo "İzinler ayarlanıyor..."
chown -R www-data:www-data $RELEASE_DIR
echo "Yeni release'e geçiliyor..."
ln -sfn $RELEASE_DIR $CURRENT_LINK
echo "PHP-FPM yeniden başlatılıyor..."
sudo systemctl reload php8.3-fpm
echo "Messenger worker'ları yeniden başlatılıyor..."
php bin/console messenger:stop-workers
echo "Eski release'ler temizleniyor..."
ls -dt /var/www/releases/*/ | tail -n +6 | xargs rm -rf
echo "Deploy tamamlandı!"# .github/workflows/deploy.yml
# GitHub Actions ile CI/CD deploy
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.shSymlink üzerinden atomik deploy anında rollback'e olanak sağlar. Yeni kodu yüklemek için Messenger worker'ları yeniden başlatılmalıdır.
API Platform ve REST
Soru 23: API Platform ile REST API nasıl oluşturulur?
API Platform, Symfony ile REST ve GraphQL API'leri oluşturmak için standart çözümdür; otomatik dokümantasyon ve HTTP standartları sunar.
// API Platform kaynak yapılandırması
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;
}// İş mantığı için özel processor
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()) {
// Yeni makale: yazar ve slug ata
$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 OpenAPI dokümantasyonunu otomatik oluşturur ve filtreler, sayfalama ve doğrulama sağlar.
Soru 24: API Platform işlemleri nasıl özelleştirilir?
API Platform, controller'lar veya State Provider/Processor ile özel işlemler oluşturmaya olanak sağlar.
// Özel işlemler
#[ApiResource(
operations: [
// Standart işlemler
new GetCollection(),
new Get(),
// Controller'lı özel işlem
new Post(
uriTemplate: '/articles/{id}/publish',
controller: PublishArticleController::class,
openapi: new Model\Operation(
summary: 'Bir makaleyi yayımlar',
description: 'Makale durumunu "published" olarak değiştirir',
),
security: "is_granted('ARTICLE_EDIT', object)",
),
// Özel State Provider'lı işlem
new GetCollection(
uriTemplate: '/articles/trending',
provider: TrendingArticlesProvider::class,
openapiContext: ['summary' => 'Popüler makaleler'],
),
],
)]
class Article
{
// ...
}// Özel işlem için controller
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('Makale zaten yayımlanmış');
}
$article->setStatus('published');
$article->setPublishedAt(new \DateTimeImmutable());
$this->entityManager->flush();
return $article;
}
}// Özel okuma mantığı için State Provider
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
{
// Popüler makaleler için özel mantık
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'lar okumayı, State Processor'lar yazmayı yönetir. Controller'lar karmaşık durumlar için kullanılabilir kalır.
Soru 25: Üretimde veritabanı migration'ları nasıl yönetilir?
Doctrine migration'ları kesintisiz çalışacak ve kolay rollback'e olanak verecek şekilde tasarlanmalıdır.
// Güvenli migration: nullable sütun ekleme
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
{
// Adım 1: nullable sütun ekle
$this->addSql('ALTER TABLE users ADD role VARCHAR(50) DEFAULT NULL');
// CONCURRENTLY ile indeks oluştur (PostgreSQL - bloklamayan)
$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');
}
}// Veri migration'ı (ayrı)
final class Version20260202100001 extends AbstractMigration
{
public function getDescription(): string
{
return 'Populate role column with default value';
}
public function up(Schema $schema): void
{
// Büyük tablolar için batch migration
$this->addSql("UPDATE users SET role = 'ROLE_USER' WHERE role IS NULL");
}
public function down(Schema $schema): void
{
// Veriler için rollback gerekmez
}
}// Son migration: sütunu NOT NULL yap
final class Version20260202100002 extends AbstractMigration
{
public function getDescription(): string
{
return 'Make role column NOT NULL';
}
public function up(Schema $schema): void
{
// Ön doğrulama
$count = $this->connection->fetchOne('SELECT COUNT(*) FROM users WHERE role IS NULL');
if ($count > 0) {
throw new \RuntimeException("$count users still have NULL role");
}
$this->addSql('ALTER TABLE users ALTER COLUMN role SET NOT NULL');
$this->addSql("ALTER TABLE users ALTER COLUMN role SET DEFAULT 'ROLE_USER'");
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE users ALTER COLUMN role DROP NOT NULL');
$this->addSql('ALTER TABLE users ALTER COLUMN role DROP DEFAULT');
}
}# Migration yönetim komutları
php bin/console doctrine:migrations:status # Migration durumu
php bin/console doctrine:migrations:migrate # Migration'ları çalıştır
php bin/console doctrine:migrations:migrate prev # Sonuncuyu geri al
php bin/console doctrine:migrations:diff # Şemadan migration üret
php bin/console doctrine:migrations:execute --down # Belirli rollbackExpand-contract stratejisi (3 deploy) sıfır kesintiyle deploy'u garanti eder: nullable ekle → veriyi taşı → kısıtlama ekle.
Sonuç
Bu 25 soru, Symfony mülakatlarının özünü kapsar — Service Container temellerinden ileri seviye üretim örüntülerine kadar.
Hazırlık kontrol listesi:
- ✅ Service Container ve dependency injection
- ✅ Doctrine ORM: ilişkiler, sorgular, filtreler
- ✅ Güvenlik: kimlik doğrulama, voter'lar, JWT
- ✅ Formlar ve özel doğrulama
- ✅ Messenger: asenkron işleme ve hata yönetimi
- ✅ Testler: birim, fonksiyonel, fixture
- ✅ Mimari: CQRS, Repository Pattern
- ✅ API Platform: REST, özel işlemler
- ✅ Üretim: performans, loglama, deploy
Her soru, Symfony resmi dokümantasyonu ile daha derin incelenmeyi hak eder. İşe alımcılar, framework'ün mimari tercihlerini anlayan ve teknik kararlarını gerekçelendirebilen adayları takdir eder.
Pratik yapmaya başla!
Mülakat simülatörleri ve teknik testlerle bilgini test et.
Etiketler
Paylaş
İlgili makaleler

Symfony 8: 2026'daki Yeni Ozellikler, PHP 8.4 Lazy Objects ve Mulakat Sorulari
Symfony 8'in yeni ozelliklerini, PHP 8.4 lazy objects entegrasyonunu, cok adimli formlari ve 2026 mulakat sorularini kod ornekleriyle kesfet.

Doctrine ORM: Symfony'de İlişkilerde Ustalaşmak
Symfony'de Doctrine ORM ilişkileri için kapsamlı rehber. OneToMany, ManyToMany, yükleme stratejileri ve pratik örneklerle performans optimizasyonu.

Symfony Live Components ve UX 3.0: 2026'da JavaScript Olmadan Reaktif Uygulamalar
Symfony Live Components ve UX 3.0 ile JavaScript yazmadan reaktif arayuzler olusturma rehberi. PHP ve Twig ile dinamik bilesenler icin kapsamli tutorial ve pratik ornekler.