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 ve PHP Mülakat Soruları - Tam Rehber

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.

Mülakat ipucu

İş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.

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

src/Service/PaymentService.phpphp
// 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;
        }
    }
}
yaml
# 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.

src/MyBundle/MyBundle.phpphp
// Ö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();
    }
}
php
// 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.

src/EventSubscriber/ApiExceptionSubscriber.phpphp
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\KernelEvents;

class ApiExceptionSubscriber implements EventSubscriberInterface
{
    // 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.

src/Entity/User.phpphp
namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: UserRepository::class)]
class User
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

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

src/Repository/ArticleRepository.phpphp
namespace App\Repository;

use App\Entity\Article;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

class ArticleRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Article::class);
    }

    // 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.

src/Doctrine/Extension/CurrentUserExtension.phpphp
// 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);
    }
}
src/Doctrine/Filter/SoftDeleteFilter.phpphp
// 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);
    }
}
yaml
# config/packages/doctrine.yaml
doctrine:
    orm:
        filters:
            soft_delete:
                class: App\Doctrine\Filter\SoftDeleteFilter
                enabled: true

Filtreler 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.

yaml
# config/packages/security.yaml
security:
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        api:
            pattern: ^/api
            stateless: true
            jwt: ~  # Lexik JWT Bundle

        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
                enable_csrf: true
            logout:
                path: app_logout
            remember_me:
                secret: '%kernel.secret%'
                lifetime: 604800

    access_control:
        - { path: ^/api/login, roles: PUBLIC_ACCESS }
        - { path: ^/api, roles: ROLE_USER }
        - { path: ^/admin, roles: ROLE_ADMIN }
src/Security/CustomAuthenticator.phpphp
// Ö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.

src/Security/Voter/ArticleVoter.phpphp
namespace App\Security\Voter;

use App\Entity\Article;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class ArticleVoter extends Voter
{
    public const VIEW = 'ARTICLE_VIEW';
    public const EDIT = 'ARTICLE_EDIT';
    public const DELETE = 'ARTICLE_DELETE';

    protected function supports(string $attribute, mixed $subject): bool
    {
        // 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());
    }
}
src/Controller/ArticleController.phpphp
// 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
{# 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.

yaml
# config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%'
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
    pass_phrase: '%env(JWT_PASSPHRASE)%'
    token_ttl: 3600  # 1 saat
src/Controller/Api/AuthController.phpphp
namespace App\Controller\Api;

use App\Entity\User;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/api')]
class AuthController extends AbstractController
{
    #[Route('/login', name: 'api_login', methods: ['POST'])]
    public function login(
        Request $request,
        UserPasswordHasherInterface $passwordHasher,
        JWTTokenManagerInterface $jwtManager,
    ): JsonResponse {
        $data = json_decode($request->getContent(), true);

        $user = $this->userRepository->findOneBy(['email' => $data['email']]);

        if (!$user || !$passwordHasher->isPasswordValid($user, $data['password'])) {
            return new JsonResponse(['error' => 'Invalid credentials'], 401);
        }

        // JWT token ü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
    }
}
src/EventListener/JWTCreatedListener.phpphp
// 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.

src/Form/ArticleType.phpphp
namespace App\Form;

use App\Entity\Article;
use App\Entity\Category;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title', TextType::class, [
                'label' => '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'],
        ]);
    }
}
src/Controller/ArticleController.phpphp
#[Route('/articles/new', name: 'article_new')]
public function new(Request $request, SluggerInterface $slugger): Response
{
    $article = new Article();
    $form = $this->createForm(ArticleType::class, $article);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        // 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.

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

src/Message/SendWelcomeEmail.phpphp
// Mesaj (verileri içeren DTO)
namespace App\Message;

final class SendWelcomeEmail
{
    public function __construct(
        public readonly int $userId,
        public readonly string $locale = 'en',
    ) {}
}
src/MessageHandler/SendWelcomeEmailHandler.phpphp
// 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);
    }
}
yaml
# config/packages/messenger.yaml
framework:
    messenger:
        failure_transport: failed

        transports:
            async:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                retry_strategy:
                    max_retries: 3
                    delay: 1000
                    multiplier: 2
            failed:
                dsn: 'doctrine://default?queue_name=failed'

        routing:
            # Mesajları async transport'a yönlendir
            App\Message\SendWelcomeEmail: async
            App\Message\ProcessImage: async
            App\Message\GenerateReport: async
src/Controller/RegistrationController.phpphp
// 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.

src/MessageHandler/ProcessPaymentHandler.phpphp
namespace App\MessageHandler;

use App\Exception\PaymentFailedException;
use App\Exception\PaymentRetryableException;
use App\Message\ProcessPayment;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;

#[AsMessageHandler]
final class ProcessPaymentHandler
{
    public function __invoke(ProcessPayment $message): void
    {
        try {
            $this->paymentGateway->process($message->orderId);
        } catch (PaymentRetryableException $e) {
            // 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
            );
        }
    }
}
src/Message/ProcessPayment.phpphp
// 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,
        };
    }
}
bash
# 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ır

Retry 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.

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

src/DataFixtures/ArticleFixtures.phpphp
namespace App\DataFixtures;

use App\Entity\Article;
use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;

class ArticleFixtures extends Fixture implements DependentFixtureInterface
{
    public function load(ObjectManager $manager): void
    {
        for ($i = 1; $i <= 20; $i++) {
            $article = new Article();
            $article->setTitle("Test Article Number $i");
            $article->setSlug("test-article-$i");
            $article->setContent("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,
        ];
    }
}
src/DataFixtures/UserFixtures.phpphp
namespace App\DataFixtures;

use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

class UserFixtures extends Fixture implements FixtureGroupInterface
{
    public function __construct(
        private readonly UserPasswordHasherInterface $passwordHasher,
    ) {}

    public function load(ObjectManager $manager): void
    {
        $users = [
            ['email' => 'admin@example.com', 'roles' => ['ROLE_ADMIN'], 'ref' => 'user-0'],
            ['email' => 'author@example.com', 'roles' => ['ROLE_AUTHOR'], 'ref' => 'user-1'],
            ['email' => 'user@example.com', 'roles' => ['ROLE_USER'], 'ref' => 'user-2'],
        ];

        foreach ($users as $userData) {
            $user = new User();
            $user->setEmail($userData['email']);
            $user->setRoles($userData['roles']);
            $user->setPassword($this->passwordHasher->hashPassword($user, 'password123'));

            $manager->persist($user);
            $this->addReference($userData['ref'], $user);
        }

        $manager->flush();
    }

    public static function getGroups(): array
    {
        return ['test', 'dev'];
    }
}
tests/Functional/ArticleControllerTest.phpphp
// 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
    }
}
bash
# 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=test

DAMADoctrineTestBundle 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.

src/Message/Command/CreateArticleCommand.phpphp
// 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 = [],
    ) {}
}
src/MessageHandler/Command/CreateArticleCommandHandler.phpphp
// 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;
    }
}
src/Message/Query/GetArticleBySlugQuery.phpphp
// 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,
    ) {}
}
src/MessageHandler/Query/GetArticleBySlugQueryHandler.phpphp
// 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;
    }
}
src/Controller/ArticleController.phpphp
// 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.

src/Repository/Contract/ArticleRepositoryInterface.phpphp
// 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;
}
src/Repository/ArticleRepository.phpphp
// 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();
    }
}
yaml
# config/services.yaml
# Arayüzü implementasyonla bağlama
services:
    App\Repository\Contract\ArticleRepositoryInterface:
        alias: App\Repository\ArticleRepository

Arayü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.

yaml
# 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
yaml
# 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
config/secrets/prod/prod.decrypt.private.phpphp
// Hassas (şifrelenmiş) secret'ların yönetimi
// php bin/console secrets:set DATABASE_URL --env=prod
// php bin/console secrets:list --reveal --env=prod
src/DependencyInjection/Configuration.phpphp
// 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;
    }
}
src/Service/ConfigurableService.phpphp
// 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ı.

config/packages/prod/doctrine.yamlphp
# Ü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
yaml
# 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'
src/Repository/ArticleRepository.phpphp
// 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();
}
bash
# Ü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-authoritative

OPcache ü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.

yaml
# config/packages/prod/monolog.yaml
monolog:
    handlers:
        main:
            type: fingers_crossed
            action_level: error
            handler: nested
            excluded_http_codes: [404, 405]
            buffer_size: 50

        nested:
            type: stream
            path: '%kernel.logs_dir%/%kernel.environment%.log'
            level: debug
            formatter: monolog.formatter.json

        console:
            type: console
            process_psr_3_messages: false
            channels: ['!event', '!doctrine']

        slack:
            type: slack
            token: '%env(SLACK_TOKEN)%'
            channel: '#alerts'
            level: critical
            bot_name: 'SymfonyBot'
src/Service/PaymentService.phpphp
// 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;
        }
    }
}
src/EventSubscriber/RequestLoggerSubscriber.phpphp
// 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.

bash
#!/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ı!"
yaml
# .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.sh

Symlink ü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.

src/Entity/Article.phpphp
// 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;
}
src/State/ArticleProcessor.phpphp
// İş 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);
    }
}
yaml
# config/packages/api_platform.yaml
api_platform:
    title: 'My API'
    version: '1.0.0'
    formats:
        jsonld: ['application/ld+json']
        json: ['application/json']
    docs_formats:
        jsonld: ['application/ld+json']
        jsonopenapi: ['application/vnd.openapi+json']
        html: ['text/html']

    defaults:
        pagination_items_per_page: 20
        pagination_client_items_per_page: true
        pagination_maximum_items_per_page: 100

    swagger:
        versions: [3]

API Platform 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.

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

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

Expand-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
Daha fazlası

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

#symfony
#php
#interview
#doctrine
#technical interview

Paylaş

İlgili makaleler