# 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. - Published: 2026-02-02 - Updated: 2026-04-27 - Author: SharpSkill - Tags: symfony, php, interview, doctrine, technical interview - Reading time: 24 min --- 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. ```php // public/index.php // 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']); }; ``` ```php // src/Kernel.php // 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. ```php // src/Service/PaymentService.php // 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. ```php // src/MyBundle/MyBundle.php // Ö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. ```php // src/EventSubscriber/ApiExceptionSubscriber.php namespace App\EventSubscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\KernelEvents; class ApiExceptionSubscriber implements EventSubscriberInterface { // 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. ```php // src/Entity/User.php namespace App\Entity; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: UserRepository::class)] class User { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; // OneToOne 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; } } ``` ```php // src/Entity/Article.php #[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. ```php // src/Repository/ArticleRepository.php namespace App\Repository; use App\Entity\Article; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; class ArticleRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Article::class); } // 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. ```php // src/Doctrine/Extension/CurrentUserExtension.php // 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); } } ``` ```php // src/Doctrine/Filter/SoftDeleteFilter.php // 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 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 } ``` ```php // src/Security/CustomAuthenticator.php // Ö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. ```php // src/Security/Voter/ArticleVoter.php namespace App\Security\Voter; use App\Entity\Article; use App\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; class ArticleVoter extends Voter { public const VIEW = 'ARTICLE_VIEW'; public const EDIT = 'ARTICLE_EDIT'; public const DELETE = 'ARTICLE_DELETE'; protected function supports(string $attribute, mixed $subject): bool { // 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()); } } ``` ```php // src/Controller/ArticleController.php // 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) %} Düzenle {% 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 ``` ```php // src/Controller/Api/AuthController.php namespace App\Controller\Api; use App\Entity\User; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; #[Route('/api')] class AuthController extends AbstractController { #[Route('/login', name: 'api_login', methods: ['POST'])] public function login( Request $request, UserPasswordHasherInterface $passwordHasher, JWTTokenManagerInterface $jwtManager, ): JsonResponse { $data = json_decode($request->getContent(), true); $user = $this->userRepository->findOneBy(['email' => $data['email']]); if (!$user || !$passwordHasher->isPasswordValid($user, $data['password'])) { return new JsonResponse(['error' => 'Invalid credentials'], 401); } // JWT token ü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 } } ``` ```php // src/EventListener/JWTCreatedListener.php // 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 ` 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. ```php // src/Form/ArticleType.php namespace App\Form; use App\Entity\Article; use App\Entity\Category; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints as Assert; class ArticleType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('title', TextType::class, [ 'label' => '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'], ]); } } ``` ```php // src/Controller/ArticleController.php #[Route('/articles/new', name: 'article_new')] public function new(Request $request, SluggerInterface $slugger): Response { $article = new Article(); $form = $this->createForm(ArticleType::class, $article); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // 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. ```php // src/Validator/UniqueEmail.php // Ö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 } ``` ```php // src/Validator/UniqueEmailValidator.php // 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(); } } } ``` ```php // src/Entity/User.php // 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. ```php // src/Message/SendWelcomeEmail.php // Mesaj (verileri içeren DTO) namespace App\Message; final class SendWelcomeEmail { public function __construct( public readonly int $userId, public readonly string $locale = 'en', ) {} } ``` ```php // src/MessageHandler/SendWelcomeEmailHandler.php // 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 ``` ```php // src/Controller/RegistrationController.php // 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. ```php // src/MessageHandler/ProcessPaymentHandler.php namespace App\MessageHandler; use App\Exception\PaymentFailedException; use App\Exception\PaymentRetryableException; use App\Message\ProcessPayment; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; #[AsMessageHandler] final class ProcessPaymentHandler { public function __invoke(ProcessPayment $message): void { try { $this->paymentGateway->process($message->orderId); } catch (PaymentRetryableException $e) { // 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 ); } } } ``` ```php // src/Message/ProcessPayment.php // 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'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. ```php // tests/Unit/Service/PriceCalculatorTest.php // 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], ]; } } ``` ```php // tests/Functional/Controller/ArticleControllerTest.php // 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. ```php // src/DataFixtures/ArticleFixtures.php namespace App\DataFixtures; use App\Entity\Article; use App\Entity\User; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Common\DataFixtures\DependentFixtureInterface; use Doctrine\Persistence\ObjectManager; class ArticleFixtures extends Fixture implements DependentFixtureInterface { public function load(ObjectManager $manager): void { for ($i = 1; $i <= 20; $i++) { $article = new Article(); $article->setTitle("Test Article Number $i"); $article->setSlug("test-article-$i"); $article->setContent("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, ]; } } ``` ```php // src/DataFixtures/UserFixtures.php namespace App\DataFixtures; use App\Entity\User; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface; use Doctrine\Persistence\ObjectManager; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; class UserFixtures extends Fixture implements FixtureGroupInterface { public function __construct( private readonly UserPasswordHasherInterface $passwordHasher, ) {} public function load(ObjectManager $manager): void { $users = [ ['email' => 'admin@example.com', 'roles' => ['ROLE_ADMIN'], 'ref' => 'user-0'], ['email' => 'author@example.com', 'roles' => ['ROLE_AUTHOR'], 'ref' => 'user-1'], ['email' => 'user@example.com', 'roles' => ['ROLE_USER'], 'ref' => 'user-2'], ]; foreach ($users as $userData) { $user = new User(); $user->setEmail($userData['email']); $user->setRoles($userData['roles']); $user->setPassword($this->passwordHasher->hashPassword($user, 'password123')); $manager->persist($user); $this->addReference($userData['ref'], $user); } $manager->flush(); } public static function getGroups(): array { return ['test', 'dev']; } } ``` ```php // tests/Functional/ArticleControllerTest.php // 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. ```php // src/Message/Command/CreateArticleCommand.php // 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 = [], ) {} } ``` ```php // src/MessageHandler/Command/CreateArticleCommandHandler.php // 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; } } ``` ```php // src/Message/Query/GetArticleBySlugQuery.php // 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, ) {} } ``` ```php // src/MessageHandler/Query/GetArticleBySlugQueryHandler.php // 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; } } ``` ```php // src/Controller/ArticleController.php // 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. ```php // src/Repository/Contract/ArticleRepositoryInterface.php // 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; } ``` ```php // src/Repository/ArticleRepository.php // 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 ``` ```php // config/secrets/prod/prod.decrypt.private.php // Hassas (şifrelenmiş) secret'ların yönetimi // php bin/console secrets:set DATABASE_URL --env=prod // php bin/console secrets:list --reveal --env=prod ``` ```php // src/DependencyInjection/Configuration.php // 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; } } ``` ```php // src/Service/ConfigurableService.php // 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ı. ```php // config/packages/prod/doctrine.yaml # Ü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' ``` ```php // src/Repository/ArticleRepository.php // 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' ``` ```php // src/Service/PaymentService.php // 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; } } } ``` ```php // src/EventSubscriber/RequestLoggerSubscriber.php // 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. ```php // src/Entity/Article.php // 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; } ``` ```php // src/State/ArticleProcessor.php // İş 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. ```php // src/Entity/Article.php // Ö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 { // ... } ``` ```php // src/Controller/PublishArticleController.php // Ö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; } } ``` ```php // src/State/TrendingArticlesProvider.php // Ö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. ```php // migrations/Version20260202100000.php // 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'); } } ``` ```php // migrations/Version20260202100001.php // 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 } } ``` ```php // migrations/Version20260202100002.php // 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/tr/blog/symfony/symfony-interview-questions