Pertanyaan Wawancara Symfony: Top 25 di 2026

25 pertanyaan wawancara Symfony paling sering ditanyakan. Arsitektur, Doctrine ORM, service, keamanan, form, dan testing dengan jawaban detail dan contoh kode.

Pertanyaan Wawancara Symfony dan PHP - Panduan Lengkap

Wawancara Symfony menguji penguasaan framework PHP profesional yang menjadi referensi, pemahaman arsitektur berbasis komponen, ORM Doctrine, dan kemampuan membangun aplikasi yang kokoh dan dapat diskalakan. Panduan ini membahas 25 pertanyaan paling sering ditanyakan, dari fundamental Symfony hingga pola produksi tingkat lanjut.

Tips wawancara

Recruiter menghargai kandidat yang memahami filosofi Symfony: pemisahan melalui service, konfigurasi eksplisit, dan kepatuhan pada standar PSR. Mampu menjelaskan keputusan arsitektural framework membuat perbedaan.

Fundamental Symfony

Pertanyaan 1: Jelaskan siklus hidup request di Symfony

Siklus hidup request Symfony melewati HTTP Kernel dan menggunakan sistem event untuk memungkinkan ekstensi pada setiap langkah. Memahaminya sangat penting untuk debugging dan menyesuaikan perilaku aplikasi.

public/index.phpphp
// Titik masuk untuk semua request HTTP
use App\Kernel;

require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

// Symfony Runtime menangani bootstrap
return function (array $context) {
    return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
src/Kernel.phpphp
// Kernel mengorkestrasi pemrosesan request
namespace App;

use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    // Kernel memuat bundle dan mengonfigurasi container
    // Event kunci dalam siklus hidup:
    // 1. kernel.request - Sebelum routing
    // 2. kernel.controller - Setelah resolusi controller
    // 3. kernel.view - Jika controller tidak mengembalikan Response
    // 4. kernel.response - Sebelum mengirim respons
    // 5. kernel.terminate - Setelah dikirim (tugas asinkron)
}

Siklus lengkap: index.php → Runtime → Kernel → HttpKernel → EventDispatcher (kernel.request) → Router → Controller → EventDispatcher (kernel.response) → Response. Setiap langkah dapat diintersep melalui Event Subscribers.

Pertanyaan 2: Apa itu Service Container dan Dependency Injection di Symfony?

Service Container (atau DIC, Dependency Injection Container) adalah jantung Symfony. Ia mengelola pembuatan instance, konfigurasi, dan injeksi semua service aplikasi.

src/Service/PaymentService.phpphp
// Service dengan dependency yang diinjeksikan secara otomatis
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, // Klien HTTP yang sudah dikonfigurasi
        private readonly OrderRepository $orderRepository,   // Repository Doctrine
        private readonly LoggerInterface $logger,            // Logger PSR-3
        private readonly string $stripeApiKey,               // Parameter yang diinjeksikan
    ) {}

    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
# Konfigurasi service
services:
    _defaults:
        autowire: true      # Injeksi otomatis berdasarkan type-hint
        autoconfigure: true # Konfigurasi tag otomatis

    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    # Konfigurasi eksplisit dengan parameter
    App\Service\PaymentService:
        arguments:
            $stripeClient: '@stripe.client'
            $stripeApiKey: '%env(STRIPE_API_KEY)%'

Autowiring secara otomatis menyelesaikan dependency berdasarkan type-hint. Parameter skalar memerlukan konfigurasi eksplisit.

Pertanyaan 3: Apa perbedaan antara Bundle dan komponen Symfony?

Bundle adalah paket yang dapat digunakan kembali yang mengintegrasikan fungsionalitas ke dalam aplikasi Symfony. Komponen adalah library PHP mandiri yang dapat digunakan tanpa Symfony.

src/MyBundle/MyBundle.phpphp
// Struktur Bundle kustom
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
{
    // Memuat konfigurasi bundle
    public function loadExtension(
        array $config,
        ContainerConfigurator $container,
        ContainerBuilder $builder
    ): void {
        // Memuat service bundle
        $container->import('../config/services.yaml');

        // Konfigurasi kondisional
        if ($config['feature_enabled']) {
            $container->services()
                ->set('my_bundle.feature_service', FeatureService::class)
                ->autowire();
        }
    }

    // Konfigurasi default bundle
    public function configure(DefinitionConfigurator $definition): void
    {
        $definition->rootNode()
            ->children()
                ->booleanNode('feature_enabled')->defaultTrue()->end()
                ->scalarNode('api_key')->isRequired()->end()
            ->end();
    }
}
php
// Penggunaan komponen tanpa Symfony
// Komponen adalah library PHP mandiri
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

// Dapat digunakan di proyek PHP apa pun
$request = Request::createFromGlobals();
$response = new Response('Hello World', 200);
$response->send();

Bundle merangkum konfigurasi, service, dan resource. Komponen adalah alat tingkat rendah yang dapat digunakan kembali di mana saja.

Pertanyaan 4: Bagaimana cara kerja Event Subscribers di Symfony?

Event Subscribers memungkinkan bereaksi pada event framework atau aplikasi, memisahkan logika bisnis dari kode utama.

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
{
    // Mendeklarasikan event yang didengarkan dan prioritasnya
    public static function getSubscribedEvents(): array
    {
        return [
            // Prioritas tinggi (dijalankan sebelum yang lain)
            KernelEvents::EXCEPTION => ['onKernelException', 100],
            KernelEvents::RESPONSE => ['onKernelResponse', 0],
        ];
    }

    public function onKernelException(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();
        $request = $event->getRequest();

        // Hanya menangani request API
        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);

        // Mengganti respons dengan JSON kita
        $event->setResponse($response);
    }

    public function onKernelResponse(\Symfony\Component\HttpKernel\Event\ResponseEvent $event): void
    {
        // Menambahkan header kustom
        $event->getResponse()->headers->set('X-Api-Version', '1.0');
    }
}

Event Subscribers ditemukan secara otomatis berkat autoconfigure. Prioritas menentukan urutan eksekusi (lebih tinggi = dijalankan lebih dulu).

Doctrine ORM

Pertanyaan 5: Jelaskan relasi Doctrine dan perbedaannya

Doctrine menawarkan beberapa jenis relasi untuk memodelkan asosiasi antar entitas. Setiap jenis berdampak pada query dan performa.

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;

    // Relasi OneToOne: satu user memiliki satu profil
    #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])]
    private ?Profile $profile = null;

    // Relasi OneToMany: satu user memiliki banyak artikel
    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Article::class, orphanRemoval: true)]
    private Collection $articles;

    // Relasi ManyToMany: banyak user memiliki banyak role
    #[ORM\ManyToMany(targetEntity: Role::class, inversedBy: 'users')]
    #[ORM\JoinTable(name: 'user_roles')]
    private Collection $roles;

    public function __construct()
    {
        // Inisialisasi koleksi yang wajib
        $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); // Sinkronisasi dua arah
        }
        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
{
    // Relasi ManyToOne: banyak artikel dimiliki oleh satu penulis
    #[ORM\ManyToOne(inversedBy: 'articles')]
    #[ORM\JoinColumn(nullable: false)]
    private ?User $author = null;

    // ManyToMany dengan atribut tambahan via entitas pivot
    #[ORM\OneToMany(mappedBy: 'article', targetEntity: ArticleTag::class)]
    private Collection $articleTags;
}

Relasi dua arah memerlukan sinkronisasi manual. Sisi "owning" (dengan JoinColumn/JoinTable) mengontrol persistence.

Pertanyaan 6: Apa itu masalah N+1 dan bagaimana cara mengatasinya dengan Doctrine?

Masalah N+1 terjadi ketika satu query utama menghasilkan N query tambahan untuk memuat relasi. Ini adalah penyebab paling umum kelambatan dalam aplikasi Symfony.

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);
    }

    // BURUK: N+1 query saat mengakses penulis
    public function findAllBad(): array
    {
        return $this->findAll();
        // + 1 query per artikel untuk memuat penulis
    }

    // BAIK: JOIN dengan eager fetch
    public function findAllWithAuthor(): array
    {
        return $this->createQueryBuilder('a')
            ->addSelect('u')              // SELECT juga penulis
            ->leftJoin('a.author', 'u')   // JOIN pada relasi
            ->orderBy('a.publishedAt', 'DESC')
            ->getQuery()
            ->getResult();
    }

    // BAIK: beberapa JOIN untuk beberapa relasi
    public function findAllWithDetails(): array
    {
        return $this->createQueryBuilder('a')
            ->addSelect('u', 'c', 't')    // SELECT semua relasi
            ->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();
    }

    // BAIK: pemuatan batch untuk daftar besar
    public function findAllOptimized(): array
    {
        $query = $this->createQueryBuilder('a')
            ->getQuery();

        // Memuat relasi dalam batch 100
        $query->setFetchMode(Article::class, 'author', ClassMetadata::FETCH_BATCH);

        return $query->getResult();
    }
}

Symfony Profiler dengan panel Doctrine membantu mendeteksi masalah N+1. Jumlah query tampil di Web Debug Toolbar.

Pertanyaan 7: Bagaimana cara membuat Query Extensions dan filter Doctrine?

Query Extensions dan filter Doctrine memungkinkan menerapkan kondisi secara otomatis ke semua query — ideal untuk multi-tenancy atau soft delete.

src/Doctrine/Extension/CurrentUserExtension.phpphp
// Extension API Platform untuk memfilter berdasarkan user secara otomatis
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
    {
        // Hanya berlaku untuk Article
        if ($resourceClass !== Article::class) {
            return;
        }

        // Admin melihat semua
        if ($this->security->isGranted('ROLE_ADMIN')) {
            return;
        }

        $user = $this->security->getUser();
        $rootAlias = $queryBuilder->getRootAliases()[0];

        // Filter otomatis berdasarkan penulis
        $queryBuilder
            ->andWhere(sprintf('%s.author = :current_user', $rootAlias))
            ->setParameter('current_user', $user);
    }
}
src/Doctrine/Filter/SoftDeleteFilter.phpphp
// Filter Doctrine global untuk mengecualikan item yang dihapus
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
    {
        // Cek apakah entitas memiliki field deletedAt
        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

Filter berlaku pada level SQL, extension pada level QueryBuilder. Nonaktifkan sementara dengan $em->getFilters()->disable('soft_delete').

Siap menguasai wawancara Symfony Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Keamanan Symfony

Pertanyaan 8: Bagaimana cara kerja sistem keamanan Symfony?

Komponen Security Symfony mengelola autentikasi (siapa user) dan otorisasi (apa yang boleh ia lakukan) melalui arsitektur yang dapat diperluas.

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
// Authenticator kustom untuk logika spesifik
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
    {
        // Authenticator ini hanya menangani request dengan X-API-KEY
        return $request->headers->has('X-API-KEY');
    }

    public function authenticate(Request $request): Passport
    {
        $apiKey = $request->headers->get('X-API-KEY');

        if (null === $apiKey) {
            throw new AuthenticationException('No API key provided');
        }

        // UserBadge memuat user berdasarkan identifier
        return new SelfValidatingPassport(
            new UserBadge($apiKey, function (string $apiKey) {
                // Logika memuat user berdasarkan API key
                return $this->userRepository->findByApiKey($apiKey);
            })
        );
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        // null = lanjutkan request secara normal
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        return new JsonResponse([
            'error' => $exception->getMessage(),
        ], Response::HTTP_UNAUTHORIZED);
    }
}

Arsitektur keamanan bertumpu pada Firewalls (konfigurasi), Authenticators (autentikasi), dan Voters (otorisasi).

Pertanyaan 9: Bagaimana cara mengimplementasikan Voter untuk otorisasi terperinci?

Voter memungkinkan logika otorisasi yang kompleks dan dapat digunakan kembali, memisahkan aturan bisnis dari kode controller.

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
    {
        // Voter ini hanya menangani Article dan atribut ini
        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;

        // Artikel yang dipublikasi terlihat untuk semua orang
        if ($attribute === self::VIEW && $article->isPublished()) {
            return true;
        }

        // Tindakan lain memerlukan user yang terautentikasi
        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
    {
        // Draft hanya terlihat oleh penulis atau admin
        return $article->getAuthor() === $user
            || in_array('ROLE_ADMIN', $user->getRoles());
    }

    private function canEdit(Article $article, User $user): bool
    {
        // Hanya penulis yang dapat mengedit
        return $article->getAuthor() === $user;
    }

    private function canDelete(Article $article, User $user): bool
    {
        // Penulis atau admin dapat menghapus
        return $article->getAuthor() === $user
            || in_array('ROLE_ADMIN', $user->getRoles());
    }
}
src/Controller/ArticleController.phpphp
// Penggunaan Voter di controller
use Symfony\Component\Security\Http\Attribute\IsGranted;

class ArticleController extends AbstractController
{
    #[Route('/articles/{id}/edit', name: 'article_edit')]
    #[IsGranted(ArticleVoter::EDIT, subject: 'article')]
    public function edit(Article $article): Response
    {
        // Otorisasi diperiksa secara otomatis
        // 403 jika voter menolak akses
    }

    // Alternatif programatik
    #[Route('/articles/{id}', name: 'article_show')]
    public function show(Article $article): Response
    {
        $this->denyAccessUnlessGranted(ArticleVoter::VIEW, $article);

        // Atau dengan kondisi
        if (!$this->isGranted(ArticleVoter::EDIT, $article)) {
            // Sembunyikan tombol edit
        }
    }
}
twig
{# Di Twig #}
{% if is_granted('ARTICLE_EDIT', article) %}
    <a href="{{ path('article_edit', {id: article.id}) }}">Edit</a>
{% endif %}

Voter ditemukan secara otomatis dan dikonsultasikan saat panggilan isGranted(). Strategi default memberi akses bila setidaknya satu Voter memberi suara positif.

Pertanyaan 10: Bagaimana cara mengamankan API dengan JWT di Symfony?

Autentikasi JWT (JSON Web Token) adalah solusi standar untuk API stateless. Symfony biasanya menggunakan LexikJWTAuthenticationBundle.

yaml
# config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%'
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
    pass_phrase: '%env(JWT_PASSPHRASE)%'
    token_ttl: 3600  # 1 jam
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);
        }

        // Buat token JWT
        $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
    {
        // Ditangani otomatis oleh bundle jika dikonfigurasi
        // Mengembalikan token baru dari refresh token
    }
}
src/EventListener/JWTCreatedListener.phpphp
// Kustomisasi payload JWT
namespace App\EventListener;

use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;

class JWTCreatedListener
{
    public function onJWTCreated(JWTCreatedEvent $event): void
    {
        $user = $event->getUser();
        $payload = $event->getData();

        // Tambahkan data kustom ke token
        $payload['user_id'] = $user->getId();
        $payload['email'] = $user->getEmail();
        $payload['permissions'] = $user->getPermissions();

        $event->setData($payload);
    }
}

Token JWT dikirim di header Authorization: Bearer <token>. Bundle secara otomatis memverifikasi tanda tangan dan kedaluwarsa.

Form Symfony

Pertanyaan 11: Bagaimana cara membuat form lanjutan dengan validasi?

Komponen Form Symfony menghasilkan form HTML, mengelola pengiriman, dan memvalidasi data dengan constraint.

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' => 'Judul artikel',
                'attr' => ['placeholder' => 'Masukkan judul...'],
                'constraints' => [
                    new Assert\NotBlank(message: 'Judul wajib diisi'),
                    new Assert\Length(
                        min: 10,
                        max: 255,
                        minMessage: 'Judul minimal {{ limit }} karakter',
                    ),
                ],
            ])
            ->add('content', TextareaType::class, [
                'label' => 'Konten',
                'constraints' => [
                    new Assert\NotBlank(),
                    new Assert\Length(min: 100),
                ],
            ])
            ->add('category', EntityType::class, [
                'class' => Category::class,
                'choice_label' => 'name',
                'placeholder' => 'Pilih kategori',
                'query_builder' => function ($repo) {
                    return $repo->createQueryBuilder('c')
                        ->where('c.active = true')
                        ->orderBy('c.name', 'ASC');
                },
            ])
            ->add('coverImage', FileType::class, [
                'label' => 'Gambar sampul',
                'mapped' => false,  // Tidak terkait dengan entitas
                'required' => false,
                'constraints' => [
                    new Assert\Image(
                        maxSize: '5M',
                        mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
                        mimeTypesMessage: 'Format gambar tidak didukung',
                    ),
                ],
            ])
            ->add('publishedAt', DateTimeType::class, [
                'widget' => 'single_text',
                'required' => false,
                'label' => 'Tanggal publikasi',
            ]);

        // Event listener untuk memodifikasi form secara dinamis
        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
            $article = $event->getData();
            $form = $event->getForm();

            // Tambahkan field hanya saat edit
            if ($article && $article->getId()) {
                $form->add('slug', TextType::class, [
                    'disabled' => true,
                    'help' => 'Slug tidak dapat diubah',
                ]);
            }
        });
    }

    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()) {
        // Penanganan upload file
        $coverImage = $form->get('coverImage')->getData();
        if ($coverImage) {
            $filename = $slugger->slug($article->getTitle()).'-'.uniqid().'.'.$coverImage->guessExtension();
            $coverImage->move($this->getParameter('covers_directory'), $filename);
            $article->setCoverImageFilename($filename);
        }

        $this->entityManager->persist($article);
        $this->entityManager->flush();

        $this->addFlash('success', 'Artikel berhasil dibuat!');
        return $this->redirectToRoute('article_show', ['id' => $article->getId()]);
    }

    return $this->render('article/new.html.twig', [
        'form' => $form,
    ]);
}

FormEvents (PRE_SET_DATA, POST_SUBMIT, dll.) memungkinkan memodifikasi field secara dinamis berdasarkan konteks.

Pertanyaan 12: Bagaimana cara mengimplementasikan validasi kustom dengan constraint?

Symfony memungkinkan membuat constraint validasi kustom untuk aturan bisnis kompleks.

src/Validator/UniqueEmail.phpphp
// Constraint kustom
namespace App\Validator;

use Symfony\Component\Validator\Constraint;

#[\Attribute(\Attribute::TARGET_PROPERTY)]
class UniqueEmail extends Constraint
{
    public string $message = 'Email "{{ value }}" sudah digunakan.';
    public ?int $excludeId = null;  // Untuk mengecualikan user saat ini ketika update
}
src/Validator/UniqueEmailValidator.phpphp
// Validator yang terkait dengan constraint
namespace App\Validator;

use App\Repository\UserRepository;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

class UniqueEmailValidator extends ConstraintValidator
{
    public function __construct(
        private readonly UserRepository $userRepository,
    ) {}

    public function validate(mixed $value, Constraint $constraint): void
    {
        if (!$constraint instanceof UniqueEmail) {
            throw new UnexpectedTypeException($constraint, UniqueEmail::class);
        }

        if (null === $value || '' === $value) {
            return; // NotBlank menangani nilai kosong
        }

        $existingUser = $this->userRepository->findOneBy(['email' => $value]);

        // Cek apakah user dengan email ini ada
        // dan bukan user saat ini (saat update)
        if ($existingUser && $existingUser->getId() !== $constraint->excludeId) {
            $this->context->buildViolation($constraint->message)
                ->setParameter('{{ value }}', $value)
                ->addViolation();
        }
    }
}
src/Entity/User.phpphp
// Penggunaan constraint pada entitas
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: 'Password harus mengandung huruf besar, huruf kecil, dan angka'
    )]
    private ?string $plainPassword = null;
}
php
// Constraint pada level kelas untuk validasi multi-field
// src/Validator/PasswordMatch.php
#[\Attribute(\Attribute::TARGET_CLASS)]
class PasswordMatch extends Constraint
{
    public string $message = 'Password tidak cocok.';

    public function getTargets(): string
    {
        return self::CLASS_CONSTRAINT;
    }
}

Constraint kustom ditemukan secara otomatis. Sufiks Validator wajib untuk validator.

Messenger dan komunikasi asinkron

Pertanyaan 13: Bagaimana cara mengimplementasikan pemrosesan asinkron dengan Messenger?

Symfony Messenger mengirim pesan ke antrian untuk pemrosesan asinkron, meningkatkan responsivitas aplikasi.

src/Message/SendWelcomeEmail.phpphp
// Pesan (DTO yang berisi data)
namespace App\Message;

final class SendWelcomeEmail
{
    public function __construct(
        public readonly int $userId,
        public readonly string $locale = 'en',
    ) {}
}
src/MessageHandler/SendWelcomeEmailHandler.phpphp
// Handler yang memproses pesan
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; // User dihapus selama proses berlangsung
        }

        $email = (new TemplatedEmail())
            ->to($user->getEmail())
            ->subject('Selamat datang di platform kami!')
            ->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:
            # Routing pesan ke transport async
            App\Message\SendWelcomeEmail: async
            App\Message\ProcessImage: async
            App\Message\GenerateReport: async
src/Controller/RegistrationController.phpphp
// Pengiriman pesan
use Symfony\Component\Messenger\MessageBusInterface;

class RegistrationController extends AbstractController
{
    #[Route('/register', name: 'app_register', methods: ['POST'])]
    public function register(
        Request $request,
        MessageBusInterface $bus,
    ): Response {
        // ... pembuatan user

        // Dispatch asinkron - email akan dikirim di background
        $bus->dispatch(new SendWelcomeEmail(
            userId: $user->getId(),
            locale: $request->getLocale(),
        ));

        // Respons cepat ke user
        return $this->redirectToRoute('app_login');
    }
}

Worker dijalankan dengan php bin/console messenger:consume async -vv. Di produksi, gunakan Supervisor untuk menjaga worker tetap berjalan.

Pertanyaan 14: Bagaimana cara menangani error dan retry dengan Messenger?

Messenger menyediakan mekanisme tangguh untuk menangani kegagalan: retry otomatis, dead letter queue, dan penanganan manual pesan yang gagal.

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) {
            // Error sementara (timeout, rate limit) → retry
            throw new RecoverableMessageHandlingException(
                $e->getMessage(),
                previous: $e
            );
        } catch (PaymentFailedException $e) {
            // Error permanen (kartu tidak valid) → tanpa retry
            throw new UnrecoverableMessageHandlingException(
                $e->getMessage(),
                previous: $e
            );
        }
    }
}
src/Message/ProcessPayment.phpphp
// Konfigurasi retry pada level pesan
namespace App\Message;

use Symfony\Component\Messenger\Stamp\DelayStamp;

final class ProcessPayment
{
    public function __construct(
        public readonly int $orderId,
        public readonly int $attempt = 1,
    ) {}

    // Penundaan retry kustom berdasarkan jumlah upaya
    public function getRetryDelay(): int
    {
        return match ($this->attempt) {
            1 => 5000,   // 5 detik
            2 => 30000,  // 30 detik
            3 => 300000, // 5 menit
            default => 600000,
        };
    }
}
bash
# Perintah pengelolaan pesan yang gagal
php bin/console messenger:failed:show          # Tampilkan pesan yang gagal
php bin/console messenger:failed:retry         # Retry semua pesan
php bin/console messenger:failed:retry 123     # Retry pesan tertentu
php bin/console messenger:failed:remove 123    # Hapus pesan

Strategi retry dan transport "failed" memastikan tidak ada pesan yang hilang. Pesan dapat dianalisis dan di-retry secara manual.

Siap menguasai wawancara Symfony Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Pengujian di Symfony

Pertanyaan 15: Bagaimana cara menyusun pengujian di Symfony?

Symfony menyediakan PHPUnit dengan helper khusus untuk menguji berbagai layer aplikasi: unit, fungsional, dan integrasi.

tests/Unit/Service/PriceCalculatorTest.phpphp
// Unit test: menguji kelas terisolasi
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
// Functional test: menguji controller via HTTP
namespace App\Tests\Functional\Controller;

use App\Entity\Article;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class ArticleControllerTest extends WebTestCase
{
    private $client;
    private EntityManagerInterface $entityManager;

    protected function setUp(): void
    {
        $this->client = static::createClient();
        $this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
    }

    public function testListArticlesReturnsOk(): void
    {
        $this->client->request('GET', '/articles');

        $this->assertResponseIsSuccessful();
        $this->assertSelectorExists('h1');
    }

    public function testCreateArticleRequiresAuthentication(): void
    {
        $this->client->request('GET', '/articles/new');

        $this->assertResponseRedirects('/login');
    }

    public function testAuthenticatedUserCanCreateArticle(): void
    {
        // Autentikasi
        $user = $this->createUser();
        $this->client->loginUser($user);

        // Akses form
        $crawler = $this->client->request('GET', '/articles/new');
        $this->assertResponseIsSuccessful();

        // Pengiriman form
        $form = $crawler->selectButton('Buat')->form([
            'article[title]' => 'Test Article Title',
            'article[content]' => 'Ini adalah konten artikel uji saya dengan jumlah karakter yang cukup.',
        ]);
        $this->client->submit($form);

        // Verifikasi
        $this->assertResponseRedirects();
        $this->client->followRedirect();
        $this->assertSelectorTextContains('h1', 'Test Article Title');

        // Verifikasi di database
        $article = $this->entityManager->getRepository(Article::class)
            ->findOneBy(['title' => 'Test Article Title']);
        $this->assertNotNull($article);
    }

    private function createUser(): User
    {
        $user = new User();
        $user->setEmail('test@example.com');
        $user->setPassword('$2y$13$hashedpassword');

        $this->entityManager->persist($user);
        $this->entityManager->flush();

        return $user;
    }

    protected function tearDown(): void
    {
        // Bersihkan database uji
        $this->entityManager->getConnection()->executeStatement('DELETE FROM article');
        $this->entityManager->getConnection()->executeStatement('DELETE FROM user');

        parent::tearDown();
    }
}

Sebaiknya pisahkan unit test (tanpa kernel), functional test (dengan kernel), dan integration test (dengan service nyata).

Pertanyaan 16: Bagaimana cara menggunakan fixtures dan DatabaseResetter?

Fixtures mengisi database dengan data uji yang realistis. Komponen DoctrineTestBundle memudahkan reset antar pengujian.

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("Konten detail artikel nomor $i...");
            $article->setStatus($i <= 15 ? 'published' : 'draft');
            $article->setPublishedAt($i <= 15 ? new \DateTimeImmutable("-$i days") : null);

            // Referensi ke user yang dibuat oleh UserFixtures
            $article->setAuthor($this->getReference('user-'.($i % 3), User::class));

            $manager->persist($article);

            // Buat referensi untuk fixtures lain
            $this->addReference("article-$i", $article);
        }

        $manager->flush();
    }

    public function getDependencies(): array
    {
        // UserFixtures harus dimuat sebelum ArticleFixtures
        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
// Penggunaan DAMADoctrineTestBundle untuk reset otomatis
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); // Sesuai fixtures
    }
}
bash
# Pemuatan fixtures
php bin/console doctrine:fixtures:load
php bin/console doctrine:fixtures:load --group=test
php bin/console doctrine:fixtures:load --env=test

DAMADoctrineTestBundle membungkus setiap pengujian dalam transaksi yang di-rollback, sehingga fixtures tidak perlu dimuat ulang antar pengujian.

Arsitektur dan pola lanjutan

Pertanyaan 17: Bagaimana cara mengimplementasikan CQRS dengan Symfony?

CQRS (Command Query Responsibility Segregation) memisahkan operasi baca dari tulis, memungkinkan optimasi independen.

src/Message/Command/CreateArticleCommand.phpphp
// Command: mewakili niat modifikasi
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: mengeksekusi modifikasi
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: mewakili permintaan baca
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: mengambil data (dapat menggunakan read model yang dioptimasi)
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
// Penggunaan Command dan Query Bus
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\HandledStamp;

class ArticleController extends AbstractController
{
    public function __construct(
        private readonly MessageBusInterface $commandBus,
        private readonly MessageBusInterface $queryBus,
    ) {}

    #[Route('/articles', methods: ['POST'])]
    public function create(Request $request): Response
    {
        $data = json_decode($request->getContent(), true);

        $envelope = $this->commandBus->dispatch(new CreateArticleCommand(
            title: $data['title'],
            content: $data['content'],
            authorId: $this->getUser()->getId(),
        ));

        $article = $envelope->last(HandledStamp::class)->getResult();

        return $this->json($article, 201);
    }

    #[Route('/articles/{slug}', methods: ['GET'])]
    public function show(string $slug): Response
    {
        $envelope = $this->queryBus->dispatch(new GetArticleBySlugQuery(
            slug: $slug,
            withComments: true,
        ));

        $article = $envelope->last(HandledStamp::class)->getResult();

        return $this->json($article);
    }
}

CQRS memungkinkan mengoptimasi pembacaan (caching, projeksi) dan penulisan (validasi, event) secara terpisah.

Pertanyaan 18: Bagaimana cara mengimplementasikan Repository Pattern dengan benar di Symfony?

Repository Pattern di Symfony sudah hadir melalui Doctrine, tetapi dapat diperkaya dengan interface dan metode bisnis.

src/Repository/Contract/ArticleRepositoryInterface.phpphp
// Interface untuk decoupling dan testabilitas
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
// Implementasi Doctrine dari repository
namespace App\Repository;

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

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

    public function findBySlug(string $slug): ?Article
    {
        return $this->createQueryBuilder('a')
            ->addSelect('u', 't')
            ->leftJoin('a.author', 'u')
            ->leftJoin('a.tags', 't')
            ->where('a.slug = :slug')
            ->setParameter('slug', $slug)
            ->getQuery()
            ->getOneOrNullResult();
    }

    public function findPublished(int $limit = 20, int $offset = 0): array
    {
        return $this->createQueryBuilder('a')
            ->addSelect('u')
            ->leftJoin('a.author', 'u')
            ->where('a.status = :status')
            ->setParameter('status', 'published')
            ->orderBy('a.publishedAt', 'DESC')
            ->setMaxResults($limit)
            ->setFirstResult($offset)
            ->getQuery()
            ->getResult();
    }

    public function findByAuthor(int $authorId): array
    {
        return $this->createQueryBuilder('a')
            ->where('a.author = :authorId')
            ->setParameter('authorId', $authorId)
            ->orderBy('a.createdAt', 'DESC')
            ->getQuery()
            ->getResult();
    }

    public function save(Article $article, bool $flush = false): void
    {
        $this->getEntityManager()->persist($article);

        if ($flush) {
            $this->getEntityManager()->flush();
        }
    }

    public function remove(Article $article, bool $flush = false): void
    {
        $this->getEntityManager()->remove($article);

        if ($flush) {
            $this->getEntityManager()->flush();
        }
    }

    // Metode query kompleks
    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
# Pengikatan interface ke implementasi
services:
    App\Repository\Contract\ArticleRepositoryInterface:
        alias: App\Repository\ArticleRepository

Interface memungkinkan membuat implementasi uji (InMemoryArticleRepository) atau mengganti sumber data tanpa mengubah kode bisnis.

Pertanyaan 19: Bagaimana cara mengelola konfigurasi dan environment di Symfony?

Symfony menggunakan sistem konfigurasi fleksibel dengan dukungan variabel environment, secret, dan file YAML per environment.

yaml
# config/packages/framework.yaml
# Konfigurasi default
framework:
    secret: '%env(APP_SECRET)%'
    http_method_override: false
    handle_all_throwables: true

    session:
        handler_id: null
        cookie_secure: auto
        cookie_samesite: lax
        storage_factory_id: session.storage.factory.native

    php_errors:
        log: true
yaml
# config/packages/prod/framework.yaml
# Override untuk produksi
framework:
    session:
        handler_id: '%env(REDIS_URL)%'
        cookie_secure: true

when@prod:
    framework:
        router:
            strict_requirements: null
config/secrets/prod/prod.decrypt.private.phpphp
// Pengelolaan secret sensitif (terenkripsi)
// php bin/console secrets:set DATABASE_URL --env=prod
// php bin/console secrets:list --reveal --env=prod
src/DependencyInjection/Configuration.phpphp
// Konfigurasi kustom untuk bundle
namespace App\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder(): TreeBuilder
    {
        $treeBuilder = new TreeBuilder('my_app');

        $treeBuilder->getRootNode()
            ->children()
                ->scalarNode('api_key')
                    ->isRequired()
                    ->cannotBeEmpty()
                ->end()
                ->integerNode('cache_ttl')
                    ->defaultValue(3600)
                    ->min(0)
                ->end()
                ->arrayNode('features')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->booleanNode('dark_mode')->defaultTrue()->end()
                        ->booleanNode('beta_features')->defaultFalse()->end()
                    ->end()
                ->end()
            ->end();

        return $treeBuilder;
    }
}
src/Service/ConfigurableService.phpphp
// Injeksi konfigurasi
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,
    ) {}
}

Secret Symfony terenkripsi dan terversi. Gunakan %env(...)% untuk variabel runtime, parameter untuk nilai statis.

Performa dan produksi

Pertanyaan 20: Bagaimana cara mengoptimasi performa aplikasi Symfony?

Optimasi mencakup beberapa tingkat: opcache, konfigurasi, cache aplikasi, dan query Doctrine.

config/packages/prod/doctrine.yamlphp
# Konfigurasi Doctrine yang dioptimasi untuk produksi
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
# Konfigurasi cache dengan Redis
framework:
    cache:
        app: cache.adapter.redis
        system: cache.adapter.system

        pools:
            doctrine.result_cache_pool:
                adapter: cache.adapter.redis
                default_lifetime: 3600
            doctrine.system_cache_pool:
                adapter: cache.adapter.system

services:
    Redis:
        class: Redis
        calls:
            - connect:
                - '%env(REDIS_HOST)%'
                - '%env(int:REDIS_PORT)%'

    Symfony\Component\Cache\Adapter\RedisAdapter:
        arguments:
            - '@Redis'
src/Repository/ArticleRepository.phpphp
// Penggunaan result cache Doctrine
public function findPopularCached(): array
{
    return $this->createQueryBuilder('a')
        ->addSelect('u')
        ->leftJoin('a.author', 'u')
        ->where('a.status = :status')
        ->setParameter('status', 'published')
        ->orderBy('a.viewCount', 'DESC')
        ->setMaxResults(10)
        ->getQuery()
        ->enableResultCache(3600, 'popular_articles')  // Cache 1 jam
        ->getResult();
}
bash
# Perintah optimasi untuk produksi
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

# Hasilkan proxy Doctrine
php bin/console doctrine:proxy:create-proxy-classes

# Kompilasi autoloader yang dioptimasi
composer install --no-dev --optimize-autoloader --classmap-authoritative

OPcache wajib aktif di produksi dengan pengaturan optimal. Warmup menghasilkan cache container dan router.

Pertanyaan 21: Bagaimana cara mengonfigurasi logging dan monitoring di Symfony?

Logging terstruktur dan monitoring yang tepat penting untuk mendiagnosis masalah di produksi.

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
// Logging terstruktur dengan konteks
namespace App\Service;

use Psr\Log\LoggerInterface;

class PaymentService
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    public function processPayment(Order $order): bool
    {
        $this->logger->info('Payment processing started', [
            'order_id' => $order->getId(),
            'amount' => $order->getTotal(),
            'currency' => $order->getCurrency(),
            'user_id' => $order->getUser()->getId(),
        ]);

        try {
            $result = $this->gateway->charge($order);

            $this->logger->info('Payment successful', [
                'order_id' => $order->getId(),
                'transaction_id' => $result->getTransactionId(),
            ]);

            return true;
        } catch (\Exception $e) {
            $this->logger->error('Payment failed', [
                'order_id' => $order->getId(),
                'error' => $e->getMessage(),
                'exception' => $e,
            ]);

            return false;
        }
    }
}
src/EventSubscriber/RequestLoggerSubscriber.phpphp
// Logging request HTTP
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),
        ]);
    }
}

Format JSON memudahkan ingest oleh tool monitoring (ELK, Datadog). Channel memungkinkan pemfilteran berdasarkan tipe log.

Pertanyaan 22: Bagaimana cara men-deploy aplikasi Symfony ke produksi?

Deploy Symfony yang andal menggabungkan persiapan build, migrasi yang aman, dan switch atomik.

bash
#!/bin/bash
# deploy.sh - Skrip deploy
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 "Membuat direktori release..."
mkdir -p $RELEASE_DIR

echo "Cloning repository..."
git clone --depth 1 --branch main git@github.com:org/repo.git $RELEASE_DIR

echo "Memasang dependency..."
cd $RELEASE_DIR
composer install --no-dev --optimize-autoloader --classmap-authoritative

echo "Menautkan file bersama..."
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 "Menjalankan migrasi..."
php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration

echo "Memanaskan cache..."
php bin/console cache:clear --env=prod --no-debug
php bin/console cache:warmup --env=prod --no-debug

echo "Mengatur izin..."
chown -R www-data:www-data $RELEASE_DIR

echo "Beralih ke release baru..."
ln -sfn $RELEASE_DIR $CURRENT_LINK

echo "Restart PHP-FPM..."
sudo systemctl reload php8.3-fpm

echo "Restart worker Messenger..."
php bin/console messenger:stop-workers

echo "Membersihkan release lama..."
ls -dt /var/www/releases/*/ | tail -n +6 | xargs rm -rf

echo "Deploy selesai!"
yaml
# .github/workflows/deploy.yml
# Deploy CI/CD dengan GitHub Actions
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: intl, pdo_pgsql, redis

      - name: Install dependencies
        run: composer install --no-dev --optimize-autoloader

      - name: Run tests
        run: php bin/phpunit

      - name: Deploy to production
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /var/www/app
            ./deploy.sh

Deploy atomik via symlink memungkinkan rollback seketika. Worker Messenger harus di-restart agar memuat kode baru.

API Platform dan REST

Pertanyaan 23: Bagaimana cara membangun API REST dengan API Platform?

API Platform adalah solusi standar untuk membuat API REST dan GraphQL dengan Symfony, menyediakan dokumentasi otomatis dan standar HTTP.

src/Entity/Article.phpphp
// Konfigurasi resource API Platform
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
// Processor kustom untuk logika bisnis
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()) {
            // Artikel baru: tetapkan penulis dan slug
            $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 secara otomatis menghasilkan dokumentasi OpenAPI dan menyediakan filter, paginasi, serta validasi.

Pertanyaan 24: Bagaimana cara menyesuaikan operasi API Platform?

API Platform memungkinkan membuat operasi kustom dengan controller atau State Provider/Processor.

src/Entity/Article.phpphp
// Operasi kustom
#[ApiResource(
    operations: [
        // Operasi standar
        new GetCollection(),
        new Get(),

        // Operasi kustom dengan controller
        new Post(
            uriTemplate: '/articles/{id}/publish',
            controller: PublishArticleController::class,
            openapi: new Model\Operation(
                summary: 'Mempublikasikan artikel',
                description: 'Mengubah status artikel menjadi "published"',
            ),
            security: "is_granted('ARTICLE_EDIT', object)",
        ),

        // Operasi dengan State Provider kustom
        new GetCollection(
            uriTemplate: '/articles/trending',
            provider: TrendingArticlesProvider::class,
            openapiContext: ['summary' => 'Artikel sedang tren'],
        ),
    ],
)]
class Article
{
    // ...
}
src/Controller/PublishArticleController.phpphp
// Controller untuk operasi kustom
namespace App\Controller;

use App\Entity\Article;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\AsController;

#[AsController]
class PublishArticleController extends AbstractController
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
    ) {}

    public function __invoke(Article $article): Article
    {
        if ($article->getStatus() === 'published') {
            throw $this->createNotFoundException('Artikel sudah dipublikasikan');
        }

        $article->setStatus('published');
        $article->setPublishedAt(new \DateTimeImmutable());

        $this->entityManager->flush();

        return $article;
    }
}
src/State/TrendingArticlesProvider.phpphp
// State Provider untuk logika baca kustom
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
    {
        // Logika kustom untuk artikel sedang tren
        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 menangani pembacaan, State Processor menangani penulisan. Controller tetap tersedia untuk kasus kompleks.

Pertanyaan 25: Bagaimana cara mengelola migrasi database di produksi?

Migrasi Doctrine harus dirancang untuk berjalan tanpa downtime dan memungkinkan rollback yang mudah.

migrations/Version20260202100000.phpphp
// Migrasi aman: menambahkan kolom nullable
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
    {
        // Langkah 1: tambahkan kolom nullable
        $this->addSql('ALTER TABLE users ADD role VARCHAR(50) DEFAULT NULL');

        // Buat indeks dengan CONCURRENTLY (PostgreSQL - tidak memblokir)
        $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
// Migrasi data (terpisah)
final class Version20260202100001 extends AbstractMigration
{
    public function getDescription(): string
    {
        return 'Populate role column with default value';
    }

    public function up(Schema $schema): void
    {
        // Migrasi batch untuk tabel besar
        $this->addSql("UPDATE users SET role = 'ROLE_USER' WHERE role IS NULL");
    }

    public function down(Schema $schema): void
    {
        // Tidak perlu rollback untuk data
    }
}
migrations/Version20260202100002.phpphp
// Migrasi terakhir: jadikan kolom NOT NULL
final class Version20260202100002 extends AbstractMigration
{
    public function getDescription(): string
    {
        return 'Make role column NOT NULL';
    }

    public function up(Schema $schema): void
    {
        // Pemeriksaan awal
        $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
# Perintah pengelolaan migrasi
php bin/console doctrine:migrations:status          # Status migrasi
php bin/console doctrine:migrations:migrate         # Jalankan migrasi
php bin/console doctrine:migrations:migrate prev    # Rollback yang terakhir
php bin/console doctrine:migrations:diff            # Generate migrasi dari schema
php bin/console doctrine:migrations:execute --down  # Rollback spesifik

Strategi expand-contract (3 deploy) menjamin deploy tanpa downtime: tambah nullable → migrasi data → tambahkan constraint.

Kesimpulan

25 pertanyaan ini mencakup esensi wawancara Symfony, dari fundamental Service Container hingga pola produksi tingkat lanjut.

Daftar persiapan:

  • ✅ Service Container dan dependency injection
  • ✅ Doctrine ORM: relasi, query, filter
  • ✅ Keamanan: autentikasi, voter, JWT
  • ✅ Form dan validasi kustom
  • ✅ Messenger: pemrosesan asinkron dan penanganan error
  • ✅ Pengujian: unit, fungsional, fixtures
  • ✅ Arsitektur: CQRS, Repository Pattern
  • ✅ API Platform: REST, operasi kustom
  • ✅ Produksi: performa, logging, deploy
Pelajari lebih lanjut

Setiap pertanyaan layak dieksplorasi lebih dalam dengan dokumentasi resmi Symfony. Recruiter menghargai kandidat yang memahami pilihan arsitektural framework dan dapat menjelaskan keputusan teknis mereka.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tag

#symfony
#php
#interview
#doctrine
#technical interview

Bagikan

Artikel terkait