Câu hỏi phỏng vấn Symfony: Top 25 năm 2026

25 câu hỏi phỏng vấn Symfony được hỏi nhiều nhất. Kiến trúc, Doctrine ORM, service, bảo mật, form và test với câu trả lời chi tiết và ví dụ code.

Câu hỏi phỏng vấn Symfony và PHP - Hướng dẫn đầy đủ

Phỏng vấn Symfony đánh giá khả năng làm chủ framework PHP chuyên nghiệp tham chiếu, sự hiểu biết về kiến trúc hướng thành phần, ORM Doctrine và năng lực xây dựng các ứng dụng vững chắc, có khả năng mở rộng. Hướng dẫn này bao quát 25 câu hỏi được hỏi nhiều nhất, từ các nền tảng Symfony đến những mẫu sản xuất nâng cao.

Mẹo cho buổi phỏng vấn

Nhà tuyển dụng đánh giá cao những ứng viên hiểu được triết lý của Symfony: tách biệt qua service, cấu hình tường minh và tuân thủ chuẩn PSR. Khả năng giải thích các lựa chọn kiến trúc của framework tạo nên khác biệt.

Nền tảng Symfony

Câu hỏi 1: Hãy giải thích vòng đời của một request trong Symfony

Vòng đời request của Symfony đi qua HTTP Kernel và sử dụng hệ thống event để cho phép mở rộng tại từng bước. Việc hiểu vòng đời này rất quan trọng cho debugging và tùy biến hành vi ứng dụng.

public/index.phpphp
// Điểm vào cho mọi request HTTP
use App\Kernel;

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

// Symfony Runtime xử lý bootstrap
return function (array $context) {
    return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
src/Kernel.phpphp
// Kernel điều phối việc xử lý request
namespace App;

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

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    // Kernel tải các bundle và cấu hình container
    // Các event chính trong vòng đời:
    // 1. kernel.request - Trước khi routing
    // 2. kernel.controller - Sau khi giải quyết controller
    // 3. kernel.view - Khi controller không trả Response
    // 4. kernel.response - Trước khi gửi response
    // 5. kernel.terminate - Sau khi đã gửi (tác vụ async)
}

Vòng đời đầy đủ: index.php → Runtime → Kernel → HttpKernel → EventDispatcher (kernel.request) → Router → Controller → EventDispatcher (kernel.response) → Response. Mỗi bước có thể được chặn thông qua Event Subscribers.

Câu hỏi 2: Service Container và Dependency Injection trong Symfony là gì?

Service Container (hay DIC, Dependency Injection Container) là trái tim của Symfony. Nó quản lý việc khởi tạo, cấu hình và inject toàn bộ các service của ứng dụng.

src/Service/PaymentService.phpphp
// Service với các dependency được inject tự động
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, // HTTP client đã cấu hình
        private readonly OrderRepository $orderRepository,   // Repository Doctrine
        private readonly LoggerInterface $logger,            // Logger PSR-3
        private readonly string $stripeApiKey,               // Tham số được inject
    ) {}

    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
# Cấu hình các service
services:
    _defaults:
        autowire: true      # Inject tự động qua type-hint
        autoconfigure: true # Cấu hình tag tự động

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

    # Cấu hình tường minh với tham số
    App\Service\PaymentService:
        arguments:
            $stripeClient: '@stripe.client'
            $stripeApiKey: '%env(STRIPE_API_KEY)%'

Autowiring tự động giải quyết dependency theo type-hint. Tham số kiểu vô hướng cần cấu hình tường minh.

Câu hỏi 3: Sự khác biệt giữa Bundle và component Symfony là gì?

Bundle là gói tái sử dụng tích hợp tính năng vào ứng dụng Symfony. Component là thư viện PHP độc lập có thể dùng mà không cần Symfony.

src/MyBundle/MyBundle.phpphp
// Cấu trúc của một Bundle tự định nghĩa
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
{
    // Tải cấu hình của bundle
    public function loadExtension(
        array $config,
        ContainerConfigurator $container,
        ContainerBuilder $builder
    ): void {
        // Tải các service của bundle
        $container->import('../config/services.yaml');

        // Cấu hình có điều kiện
        if ($config['feature_enabled']) {
            $container->services()
                ->set('my_bundle.feature_service', FeatureService::class)
                ->autowire();
        }
    }

    // Cấu hình mặc định của bundle
    public function configure(DefinitionConfigurator $definition): void
    {
        $definition->rootNode()
            ->children()
                ->booleanNode('feature_enabled')->defaultTrue()->end()
                ->scalarNode('api_key')->isRequired()->end()
            ->end();
    }
}
php
// Sử dụng component mà không cần Symfony
// Component là thư viện PHP độc lập
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

// Có thể dùng trong bất kỳ project PHP nào
$request = Request::createFromGlobals();
$response = new Response('Hello World', 200);
$response->send();

Bundle đóng gói cấu hình, service và tài nguyên. Component là công cụ cấp thấp có thể tái sử dụng ở bất cứ đâu.

Câu hỏi 4: Event Subscribers trong Symfony hoạt động thế nào?

Event Subscribers cho phép phản ứng với event của framework hay của ứng dụng, tách logic nghiệp vụ khỏi code chính.

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
{
    // Khai báo các event đăng ký và thứ tự ưu tiên
    public static function getSubscribedEvents(): array
    {
        return [
            // Ưu tiên cao (chạy trước các listener khác)
            KernelEvents::EXCEPTION => ['onKernelException', 100],
            KernelEvents::RESPONSE => ['onKernelResponse', 0],
        ];
    }

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

        // Chỉ xử lý 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);

        // Thay response bằng JSON của chúng ta
        $event->setResponse($response);
    }

    public function onKernelResponse(\Symfony\Component\HttpKernel\Event\ResponseEvent $event): void
    {
        // Thêm header tùy chỉnh
        $event->getResponse()->headers->set('X-Api-Version', '1.0');
    }
}

Event Subscribers được phát hiện tự động nhờ autoconfigure. Mức độ ưu tiên xác định thứ tự thực thi (cao hơn = chạy trước).

Doctrine ORM

Câu hỏi 5: Hãy giải thích các quan hệ Doctrine và sự khác biệt

Doctrine cung cấp nhiều loại quan hệ để mô hình hóa liên kết giữa các entity. Mỗi loại có ảnh hưởng đến truy vấn và hiệu năng.

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;

    // Quan hệ OneToOne: một user có một profile
    #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])]
    private ?Profile $profile = null;

    // Quan hệ OneToMany: một user có nhiều bài viết
    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Article::class, orphanRemoval: true)]
    private Collection $articles;

    // Quan hệ ManyToMany: nhiều user có nhiều role
    #[ORM\ManyToMany(targetEntity: Role::class, inversedBy: 'users')]
    #[ORM\JoinTable(name: 'user_roles')]
    private Collection $roles;

    public function __construct()
    {
        // Khởi tạo collection bắt buộc
        $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); // Đồng bộ hai chiều
        }
        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
{
    // Quan hệ ManyToOne: nhiều bài viết thuộc về một tác giả
    #[ORM\ManyToOne(inversedBy: 'articles')]
    #[ORM\JoinColumn(nullable: false)]
    private ?User $author = null;

    // ManyToMany có thuộc tính bổ sung qua entity pivot
    #[ORM\OneToMany(mappedBy: 'article', targetEntity: ArticleTag::class)]
    private Collection $articleTags;
}

Quan hệ hai chiều cần đồng bộ thủ công. Phía "owning" (có JoinColumn/JoinTable) điều khiển việc lưu trữ.

Câu hỏi 6: Vấn đề N+1 là gì và cách giải quyết bằng Doctrine?

Vấn đề N+1 xảy ra khi một truy vấn chính sinh ra N truy vấn bổ sung để tải các quan hệ. Đây là nguyên nhân phổ biến nhất gây chậm trong các ứng dụng 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);
    }

    // KÉM: N+1 truy vấn nếu truy cập tác giả
    public function findAllBad(): array
    {
        return $this->findAll();
        // + 1 truy vấn mỗi bài để tải tác giả
    }

    // TỐT: JOIN với eager fetch
    public function findAllWithAuthor(): array
    {
        return $this->createQueryBuilder('a')
            ->addSelect('u')              // SELECT cả tác giả
            ->leftJoin('a.author', 'u')   // JOIN trên quan hệ
            ->orderBy('a.publishedAt', 'DESC')
            ->getQuery()
            ->getResult();
    }

    // TỐT: nhiều JOIN cho nhiều quan hệ
    public function findAllWithDetails(): array
    {
        return $this->createQueryBuilder('a')
            ->addSelect('u', 'c', 't')    // SELECT toàn bộ quan hệ
            ->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();
    }

    // TỐT: tải theo lô cho danh sách lớn
    public function findAllOptimized(): array
    {
        $query = $this->createQueryBuilder('a')
            ->getQuery();

        // Tải các quan hệ theo lô 100
        $query->setFetchMode(Article::class, 'author', ClassMetadata::FETCH_BATCH);

        return $query->getResult();
    }
}

Symfony Profiler với panel Doctrine giúp phát hiện vấn đề N+1. Số lượng truy vấn xuất hiện trong Web Debug Toolbar.

Câu hỏi 7: Làm thế nào để tạo Query Extensions và filter Doctrine?

Query Extensions và filter Doctrine cho phép tự động áp dụng điều kiện cho mọi truy vấn — lý tưởng cho multi-tenancy hay soft delete.

src/Doctrine/Extension/CurrentUserExtension.phpphp
// Extension API Platform để lọc tự động theo user
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
    {
        // Chỉ áp dụng cho Article
        if ($resourceClass !== Article::class) {
            return;
        }

        // Admin thấy mọi thứ
        if ($this->security->isGranted('ROLE_ADMIN')) {
            return;
        }

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

        // Tự động lọc theo tác giả
        $queryBuilder
            ->andWhere(sprintf('%s.author = :current_user', $rootAlias))
            ->setParameter('current_user', $user);
    }
}
src/Doctrine/Filter/SoftDeleteFilter.phpphp
// Filter Doctrine toàn cục để loại trừ phần tử đã xóa
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
    {
        // Kiểm tra entity có trường deletedAt không
        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 áp dụng ở cấp SQL, extension áp dụng ở cấp QueryBuilder. Có thể tạm vô hiệu hóa với $em->getFilters()->disable('soft_delete').

Sẵn sàng chinh phục phỏng vấn Symfony?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Bảo mật Symfony

Câu hỏi 8: Hệ thống bảo mật của Symfony hoạt động thế nào?

Component Security của Symfony quản lý xác thực (user là ai) và phân quyền (user có thể làm gì) thông qua kiến trúc có thể mở rộng.

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 tùy chỉnh cho logic riêng
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 này chỉ xử lý request có 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 tải user theo identifier
        return new SelfValidatingPassport(
            new UserBadge($apiKey, function (string $apiKey) {
                // Logic tải user theo API key
                return $this->userRepository->findByApiKey($apiKey);
            })
        );
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        // null = tiếp tục request bình thường
        return null;
    }

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

Kiến trúc bảo mật dựa trên Firewalls (cấu hình), Authenticators (xác thực) và Voters (phân quyền).

Câu hỏi 9: Làm thế nào để triển khai Voters cho phân quyền chi tiết?

Voters cho phép viết logic phân quyền phức tạp và tái sử dụng, tách quy tắc nghiệp vụ khỏi code 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 này chỉ xử lý Article và các thuộc tính này
        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;

        // Bài đã xuất bản hiển thị cho mọi người
        if ($attribute === self::VIEW && $article->isPublished()) {
            return true;
        }

        // Các hành động khác cần user đã xác thực
        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
    {
        // Bản nháp chỉ tác giả hoặc admin nhìn thấy
        return $article->getAuthor() === $user
            || in_array('ROLE_ADMIN', $user->getRoles());
    }

    private function canEdit(Article $article, User $user): bool
    {
        // Chỉ tác giả mới được sửa
        return $article->getAuthor() === $user;
    }

    private function canDelete(Article $article, User $user): bool
    {
        // Tác giả hoặc admin được xóa
        return $article->getAuthor() === $user
            || in_array('ROLE_ADMIN', $user->getRoles());
    }
}
src/Controller/ArticleController.phpphp
// Sử dụng Voter trong 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
    {
        // Phân quyền được kiểm tra tự động
        // 403 nếu voter từ chối quyền truy cập
    }

    // Cách thay thế kiểu lập trình
    #[Route('/articles/{id}', name: 'article_show')]
    public function show(Article $article): Response
    {
        $this->denyAccessUnlessGranted(ArticleVoter::VIEW, $article);

        // Hoặc kèm điều kiện
        if (!$this->isGranted(ArticleVoter::EDIT, $article)) {
            // Ẩn nút sửa
        }
    }
}
twig
{# Trong Twig #}
{% if is_granted('ARTICLE_EDIT', article) %}
    <a href="{{ path('article_edit', {id: article.id}) }}">Sửa</a>
{% endif %}

Voters được phát hiện tự động và tham vấn ở mỗi lần isGranted(). Chiến lược mặc định cho phép truy cập nếu có ít nhất một Voter bỏ phiếu thuận.

Câu hỏi 10: Làm thế nào để bảo mật API bằng JWT trong Symfony?

Xác thực JWT (JSON Web Token) là giải pháp tiêu chuẩn cho API stateless. Symfony thường dùng 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 giờ
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);
        }

        // Tạo 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
    {
        // Bundle xử lý tự động nếu được cấu hình
        // Trả token mới từ refresh token
    }
}
src/EventListener/JWTCreatedListener.phpphp
// Tùy biến 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();

        // Thêm dữ liệu tùy chỉnh vào token
        $payload['user_id'] = $user->getId();
        $payload['email'] = $user->getEmail();
        $payload['permissions'] = $user->getPermissions();

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

Token JWT được gửi trong header Authorization: Bearer <token>. Bundle tự động xác minh chữ ký và thời hạn.

Form Symfony

Câu hỏi 11: Làm thế nào để tạo form nâng cao có validation?

Component Form của Symfony tạo form HTML, xử lý gửi và xác thực dữ liệu bằng 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' => 'Tiêu đề bài viết',
                'attr' => ['placeholder' => 'Nhập tiêu đề...'],
                'constraints' => [
                    new Assert\NotBlank(message: 'Tiêu đề là bắt buộc'),
                    new Assert\Length(
                        min: 10,
                        max: 255,
                        minMessage: 'Tiêu đề phải có ít nhất {{ limit }} ký tự',
                    ),
                ],
            ])
            ->add('content', TextareaType::class, [
                'label' => 'Nội dung',
                'constraints' => [
                    new Assert\NotBlank(),
                    new Assert\Length(min: 100),
                ],
            ])
            ->add('category', EntityType::class, [
                'class' => Category::class,
                'choice_label' => 'name',
                'placeholder' => 'Chọn danh mục',
                'query_builder' => function ($repo) {
                    return $repo->createQueryBuilder('c')
                        ->where('c.active = true')
                        ->orderBy('c.name', 'ASC');
                },
            ])
            ->add('coverImage', FileType::class, [
                'label' => 'Ảnh bìa',
                'mapped' => false,  // Không gắn với entity
                'required' => false,
                'constraints' => [
                    new Assert\Image(
                        maxSize: '5M',
                        mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
                        mimeTypesMessage: 'Định dạng ảnh không được hỗ trợ',
                    ),
                ],
            ])
            ->add('publishedAt', DateTimeType::class, [
                'widget' => 'single_text',
                'required' => false,
                'label' => 'Ngày xuất bản',
            ]);

        // Event listener để chỉnh sửa form động
        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
            $article = $event->getData();
            $form = $event->getForm();

            // Chỉ thêm trường khi sửa
            if ($article && $article->getId()) {
                $form->add('slug', TextType::class, [
                    'disabled' => true,
                    'help' => 'Slug không thể sửa',
                ]);
            }
        });
    }

    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()) {
        // Xử lý 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', 'Tạo bài viết thành công!');
        return $this->redirectToRoute('article_show', ['id' => $article->getId()]);
    }

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

FormEvents (PRE_SET_DATA, POST_SUBMIT, v.v.) cho phép thay đổi trường động theo ngữ cảnh.

Câu hỏi 12: Làm thế nào để tạo validation tùy chỉnh với constraint?

Symfony cho phép tạo constraint validation tùy chỉnh cho các quy tắc nghiệp vụ phức tạp.

src/Validator/UniqueEmail.phpphp
// Constraint tùy chỉnh
namespace App\Validator;

use Symfony\Component\Validator\Constraint;

#[\Attribute(\Attribute::TARGET_PROPERTY)]
class UniqueEmail extends Constraint
{
    public string $message = 'Email "{{ value }}" đã được sử dụng.';
    public ?int $excludeId = null;  // Để loại trừ user hiện tại khi cập nhật
}
src/Validator/UniqueEmailValidator.phpphp
// Validator gắn với 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 xử lý giá trị rỗng
        }

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

        // Kiểm tra có user dùng email này không
        // và không phải user hiện tại (khi cập nhật)
        if ($existingUser && $existingUser->getId() !== $constraint->excludeId) {
            $this->context->buildViolation($constraint->message)
                ->setParameter('{{ value }}', $value)
                ->addViolation();
        }
    }
}
src/Entity/User.phpphp
// Sử dụng constraint trên entity
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: 'Mật khẩu phải có chữ hoa, chữ thường và số'
    )]
    private ?string $plainPassword = null;
}
php
// Constraint cấp class cho validation nhiều trường
// src/Validator/PasswordMatch.php
#[\Attribute(\Attribute::TARGET_CLASS)]
class PasswordMatch extends Constraint
{
    public string $message = 'Mật khẩu không khớp.';

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

Constraint tùy chỉnh được phát hiện tự động. Hậu tố Validator là bắt buộc cho validator.

Messenger và giao tiếp bất đồng bộ

Câu hỏi 13: Làm thế nào để xử lý bất đồng bộ với Messenger?

Symfony Messenger gửi message vào hàng đợi để xử lý bất đồng bộ, cải thiện khả năng phản hồi của ứng dụng.

src/Message/SendWelcomeEmail.phpphp
// Message (DTO chứa dữ liệu)
namespace App\Message;

final class SendWelcomeEmail
{
    public function __construct(
        public readonly int $userId,
        public readonly string $locale = 'en',
    ) {}
}
src/MessageHandler/SendWelcomeEmailHandler.phpphp
// Handler xử lý message
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 đã bị xóa trong thời gian này
        }

        $email = (new TemplatedEmail())
            ->to($user->getEmail())
            ->subject('Chào mừng đến với nền tảng của chúng tôi!')
            ->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 message đến transport async
            App\Message\SendWelcomeEmail: async
            App\Message\ProcessImage: async
            App\Message\GenerateReport: async
src/Controller/RegistrationController.phpphp
// Dispatch message
use Symfony\Component\Messenger\MessageBusInterface;

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

        // Dispatch bất đồng bộ - email gửi ở nền
        $bus->dispatch(new SendWelcomeEmail(
            userId: $user->getId(),
            locale: $request->getLocale(),
        ));

        // Phản hồi tức thì cho user
        return $this->redirectToRoute('app_login');
    }
}

Khởi động worker bằng php bin/console messenger:consume async -vv. Trong production, dùng Supervisor để giữ worker hoạt động.

Câu hỏi 14: Làm thế nào để xử lý lỗi và retry với Messenger?

Messenger cung cấp cơ chế bền bỉ để xử lý sự cố: retry tự động, dead letter queue và xử lý thủ công các message thất bại.

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) {
            // Lỗi tạm thời (timeout, rate limit) → retry
            throw new RecoverableMessageHandlingException(
                $e->getMessage(),
                previous: $e
            );
        } catch (PaymentFailedException $e) {
            // Lỗi vĩnh viễn (thẻ không hợp lệ) → không retry
            throw new UnrecoverableMessageHandlingException(
                $e->getMessage(),
                previous: $e
            );
        }
    }
}
src/Message/ProcessPayment.phpphp
// Cấu hình retry ở cấp message
namespace App\Message;

use Symfony\Component\Messenger\Stamp\DelayStamp;

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

    // Độ trễ retry tùy theo số lần thử
    public function getRetryDelay(): int
    {
        return match ($this->attempt) {
            1 => 5000,   // 5 giây
            2 => 30000,  // 30 giây
            3 => 300000, // 5 phút
            default => 600000,
        };
    }
}
bash
# Lệnh quản lý message thất bại
php bin/console messenger:failed:show          # Liệt kê message thất bại
php bin/console messenger:failed:retry         # Retry mọi message
php bin/console messenger:failed:retry 123     # Retry message cụ thể
php bin/console messenger:failed:remove 123    # Xóa một message

Chiến lược retry và transport "failed" đảm bảo không có message nào bị mất. Có thể phân tích và retry message thủ công.

Sẵn sàng chinh phục phỏng vấn Symfony?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Test trong Symfony

Câu hỏi 15: Làm thế nào để cấu trúc test trong Symfony?

Symfony cung cấp PHPUnit cùng các helper chuyên biệt để test các tầng khác nhau của ứng dụng: unit, functional và integration.

tests/Unit/Service/PriceCalculatorTest.phpphp
// Unit test: kiểm thử một class độc lập
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: kiểm thử controller qua 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
    {
        // Xác thực
        $user = $this->createUser();
        $this->client->loginUser($user);

        // Truy cập form
        $crawler = $this->client->request('GET', '/articles/new');
        $this->assertResponseIsSuccessful();

        // Gửi form
        $form = $crawler->selectButton('Tạo')->form([
            'article[title]' => 'Test Article Title',
            'article[content]' => 'Đây là nội dung bài viết test với đủ ký tự.',
        ]);
        $this->client->submit($form);

        // Kiểm chứng
        $this->assertResponseRedirects();
        $this->client->followRedirect();
        $this->assertSelectorTextContains('h1', 'Test Article Title');

        // Kiểm tra trong 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
    {
        // Dọn database test
        $this->entityManager->getConnection()->executeStatement('DELETE FROM article');
        $this->entityManager->getConnection()->executeStatement('DELETE FROM user');

        parent::tearDown();
    }
}

Nên tách rõ unit test (không kernel), functional test (có kernel) và integration test (service thật).

Câu hỏi 16: Làm thế nào để dùng fixture và DatabaseResetter?

Fixtures nạp database với dữ liệu test thực tế. Component DoctrineTestBundle giúp reset giữa các test.

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("Nội dung chi tiết của bài số $i...");
            $article->setStatus($i <= 15 ? 'published' : 'draft');
            $article->setPublishedAt($i <= 15 ? new \DateTimeImmutable("-$i days") : null);

            // Tham chiếu đến user do UserFixtures tạo
            $article->setAuthor($this->getReference('user-'.($i % 3), User::class));

            $manager->persist($article);

            // Tạo tham chiếu cho fixture khác
            $this->addReference("article-$i", $article);
        }

        $manager->flush();
    }

    public function getDependencies(): array
    {
        // UserFixtures phải nạp trước 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
// Sử dụng DAMADoctrineTestBundle để reset tự động
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); // Theo fixtures
    }
}
bash
# Nạp fixtures
php bin/console doctrine:fixtures:load
php bin/console doctrine:fixtures:load --group=test
php bin/console doctrine:fixtures:load --env=test

DAMADoctrineTestBundle bọc mỗi test trong một transaction được rollback, nhờ vậy không cần nạp lại fixtures giữa các test.

Kiến trúc và mẫu nâng cao

Câu hỏi 17: Làm thế nào để triển khai CQRS với Symfony?

CQRS (Command Query Responsibility Segregation) tách thao tác đọc và ghi, cho phép tối ưu độc lập.

src/Message/Command/CreateArticleCommand.phpphp
// Command: đại diện ý định thay đổi
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: thực hiện thay đổi
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: đại diện yêu cầu đọc
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: lấy dữ liệu (có thể dùng read model tối ưu)
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
// Sử dụng Command Bus và 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 giúp tối ưu việc đọc (caching, projection) và ghi (validation, event) một cách riêng biệt.

Câu hỏi 18: Làm thế nào để triển khai Repository Pattern đúng cách trong Symfony?

Repository Pattern đã có sẵn trong Symfony qua Doctrine, nhưng có thể bổ sung interface và phương thức nghiệp vụ.

src/Repository/Contract/ArticleRepositoryInterface.phpphp
// Interface để giảm phụ thuộc và dễ test
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
// Triển khai Doctrine của 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();
        }
    }

    // Phương thức truy vấn phức tạp
    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
# Liên kết interface với triển khai
services:
    App\Repository\Contract\ArticleRepositoryInterface:
        alias: App\Repository\ArticleRepository

Interface giúp tạo triển khai test (InMemoryArticleRepository) hoặc đổi nguồn dữ liệu mà không thay đổi code nghiệp vụ.

Câu hỏi 19: Làm thế nào để quản lý cấu hình và môi trường trong Symfony?

Symfony dùng hệ thống cấu hình linh hoạt với hỗ trợ biến môi trường, secret và file YAML cho từng môi trường.

yaml
# config/packages/framework.yaml
# Cấu hình mặc định
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 cho production
framework:
    session:
        handler_id: '%env(REDIS_URL)%'
        cookie_secure: true

when@prod:
    framework:
        router:
            strict_requirements: null
config/secrets/prod/prod.decrypt.private.phpphp
// Quản lý secret nhạy cảm (đã mã hóa)
// php bin/console secrets:set DATABASE_URL --env=prod
// php bin/console secrets:list --reveal --env=prod
src/DependencyInjection/Configuration.phpphp
// Cấu hình tùy chỉnh cho 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
// Inject cấu hình
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 trong Symfony được mã hóa và quản lý phiên bản. Dùng %env(...)% cho biến runtime, dùng tham số cho giá trị tĩnh.

Hiệu năng và sản xuất

Câu hỏi 20: Làm thế nào để tối ưu hiệu năng ứng dụng Symfony?

Tối ưu trải dài nhiều mức: opcache, cấu hình, cache ứng dụng và truy vấn Doctrine.

config/packages/prod/doctrine.yamlphp
# Cấu hình Doctrine tối ưu cho production
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
# Cấu hình cache với 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
// Sử dụng result cache của 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 giờ
        ->getResult();
}
bash
# Lệnh tối ưu cho production
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

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

# Biên dịch autoloader tối ưu
composer install --no-dev --optimize-autoloader --classmap-authoritative

OPcache phải được bật trong production với cấu hình tối ưu. Warmup tạo cache cho container và router.

Câu hỏi 21: Làm thế nào để cấu hình logging và giám sát trong Symfony?

Logging có cấu trúc và giám sát phù hợp là yếu tố thiết yếu để chẩn đoán vấn đề trong production.

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 có cấu trúc kèm context
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 cho 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),
        ]);
    }
}

Định dạng JSON giúp các công cụ giám sát (ELK, Datadog) dễ ingest. Channel cho phép lọc theo loại log.

Câu hỏi 22: Làm thế nào để deploy ứng dụng Symfony lên production?

Một deploy Symfony bền vững kết hợp chuẩn bị build, migration an toàn và chuyển đổi atomic.

bash
#!/bin/bash
# deploy.sh - Script 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 "Tạo thư mục release..."
mkdir -p $RELEASE_DIR

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

echo "Cài đặt dependency..."
cd $RELEASE_DIR
composer install --no-dev --optimize-autoloader --classmap-authoritative

echo "Liên kết file dùng chung..."
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 "Chạy migration..."
php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration

echo "Khởi động cache..."
php bin/console cache:clear --env=prod --no-debug
php bin/console cache:warmup --env=prod --no-debug

echo "Đặt quyền..."
chown -R www-data:www-data $RELEASE_DIR

echo "Chuyển sang release mới..."
ln -sfn $RELEASE_DIR $CURRENT_LINK

echo "Khởi động lại PHP-FPM..."
sudo systemctl reload php8.3-fpm

echo "Khởi động lại worker Messenger..."
php bin/console messenger:stop-workers

echo "Dọn release cũ..."
ls -dt /var/www/releases/*/ | tail -n +6 | xargs rm -rf

echo "Deploy hoàn tất!"
yaml
# .github/workflows/deploy.yml
# Deploy CI/CD với 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 atomic qua symlink cho phép rollback ngay lập tức. Worker Messenger phải được khởi động lại để nạp code mới.

API Platform và REST

Câu hỏi 23: Làm thế nào để xây dựng REST API với API Platform?

API Platform là giải pháp chuẩn để tạo API REST và GraphQL với Symfony, kèm theo tài liệu tự động và chuẩn HTTP.

src/Entity/Article.phpphp
// Cấu hình 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 tùy chỉnh cho logic nghiệp vụ
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()) {
            // Bài mới: gán tác giả và 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 tạo tài liệu OpenAPI tự động và cung cấp filter, phân trang, validation.

Câu hỏi 24: Làm thế nào để tùy chỉnh các operation của API Platform?

API Platform cho phép tạo operation tùy chỉnh bằng controller hoặc State Provider/Processor.

src/Entity/Article.phpphp
// Operation tùy chỉnh
#[ApiResource(
    operations: [
        // Operation chuẩn
        new GetCollection(),
        new Get(),

        // Operation tùy chỉnh với controller
        new Post(
            uriTemplate: '/articles/{id}/publish',
            controller: PublishArticleController::class,
            openapi: new Model\Operation(
                summary: 'Xuất bản bài viết',
                description: 'Đổi trạng thái bài viết sang "published"',
            ),
            security: "is_granted('ARTICLE_EDIT', object)",
        ),

        // Operation với State Provider tùy chỉnh
        new GetCollection(
            uriTemplate: '/articles/trending',
            provider: TrendingArticlesProvider::class,
            openapiContext: ['summary' => 'Bài viết xu hướng'],
        ),
    ],
)]
class Article
{
    // ...
}
src/Controller/PublishArticleController.phpphp
// Controller cho operation tùy chỉnh
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('Bài đã được xuất bản');
        }

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

        $this->entityManager->flush();

        return $article;
    }
}
src/State/TrendingArticlesProvider.phpphp
// State Provider cho logic đọc tùy chỉnh
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
    {
        // Logic tùy chỉnh cho bài viết xu hướng
        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 xử lý đọc, State Processor xử lý ghi. Controller vẫn khả dụng cho các trường hợp phức tạp.

Câu hỏi 25: Làm thế nào để quản lý migration database trên production?

Migration Doctrine phải được thiết kế để chạy không downtime và cho phép rollback đơn giản.

migrations/Version20260202100000.phpphp
// Migration an toàn: thêm cột 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
    {
        // Bước 1: thêm cột nullable
        $this->addSql('ALTER TABLE users ADD role VARCHAR(50) DEFAULT NULL');

        // Tạo chỉ mục CONCURRENTLY (PostgreSQL - không block)
        $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
// Migration dữ liệu (tách riêng)
final class Version20260202100001 extends AbstractMigration
{
    public function getDescription(): string
    {
        return 'Populate role column with default value';
    }

    public function up(Schema $schema): void
    {
        // Migration theo lô cho bảng lớn
        $this->addSql("UPDATE users SET role = 'ROLE_USER' WHERE role IS NULL");
    }

    public function down(Schema $schema): void
    {
        // Không cần rollback dữ liệu
    }
}
migrations/Version20260202100002.phpphp
// Migration cuối: chuyển cột thành NOT NULL
final class Version20260202100002 extends AbstractMigration
{
    public function getDescription(): string
    {
        return 'Make role column NOT NULL';
    }

    public function up(Schema $schema): void
    {
        // Kiểm tra trước
        $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
# Lệnh quản lý migration
php bin/console doctrine:migrations:status          # Trạng thái migration
php bin/console doctrine:migrations:migrate         # Chạy migration
php bin/console doctrine:migrations:migrate prev    # Rollback migration cuối
php bin/console doctrine:migrations:diff            # Sinh migration từ schema
php bin/console doctrine:migrations:execute --down  # Rollback cụ thể

Chiến lược expand-contract (3 deploy) đảm bảo deploy không downtime: thêm nullable → migration dữ liệu → thêm constraint.

Kết luận

25 câu hỏi này bao quát những điểm cốt lõi của các buổi phỏng vấn Symfony — từ nền tảng Service Container đến các mẫu sản xuất nâng cao.

Danh mục chuẩn bị:

  • ✅ Service Container và dependency injection
  • ✅ Doctrine ORM: quan hệ, truy vấn, filter
  • ✅ Bảo mật: xác thực, voter, JWT
  • ✅ Form và validation tùy chỉnh
  • ✅ Messenger: xử lý bất đồng bộ và xử lý lỗi
  • ✅ Test: unit, functional, fixtures
  • ✅ Kiến trúc: CQRS, Repository Pattern
  • ✅ API Platform: REST, operation tùy chỉnh
  • ✅ Production: hiệu năng, logging, deploy
Tìm hiểu sâu hơn

Mỗi câu hỏi đều xứng đáng được nghiên cứu sâu hơn cùng tài liệu chính thức của Symfony. Nhà tuyển dụng đánh giá cao những ứng viên hiểu các lựa chọn kiến trúc của framework và biết cách bảo vệ quyết định kỹ thuật của mình.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

#symfony
#php
#interview
#doctrine
#technical interview

Chia sẻ

Bài viết liên quan