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.

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.
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.
// Đ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']);
};// 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.
// 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;
}
}
}# 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.
// 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();
}
}// 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.
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.
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;
}
}#[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.
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.
// 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);
}
}// 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);
}
}# config/packages/doctrine.yaml
doctrine:
orm:
filters:
soft_delete:
class: App\Doctrine\Filter\SoftDeleteFilter
enabled: trueFilter á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.
# 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 }// 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.
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());
}
}// 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
}
}
}{# 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.
# 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ờ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
}
}// 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.
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'],
]);
}
}#[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.
// 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
}// 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();
}
}
}// 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;
}// 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.
// 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',
) {}
}// 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);
}
}# 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// 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.
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
);
}
}
}// 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,
};
}
}# 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 messageChiế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.
// 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],
];
}
}// 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.
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,
];
}
}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'];
}
}// 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
}
}# Nạp fixtures
php bin/console doctrine:fixtures:load
php bin/console doctrine:fixtures:load --group=test
php bin/console doctrine:fixtures:load --env=testDAMADoctrineTestBundle 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.
// 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 = [],
) {}
}// 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;
}
}// 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,
) {}
}// 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;
}
}// 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ụ.
// 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;
}// 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();
}
}# config/services.yaml
# Liên kết interface với triển khai
services:
App\Repository\Contract\ArticleRepositoryInterface:
alias: App\Repository\ArticleRepositoryInterface 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.
# 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# 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// 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// 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;
}
}// 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.
# 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# 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'// 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();
}# 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-authoritativeOPcache 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.
# 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'// 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;
}
}
}// 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.
#!/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!"# .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.shDeploy 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.
// 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;
}// 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);
}
}# 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.
// 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
{
// ...
}// 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;
}
}// 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.
// 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');
}
}// 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
}
}// 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');
}
}# 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
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ẻ
Chia sẻ
Bài viết liên quan

Doctrine ORM: Làm chủ các quan hệ trong Symfony
Hướng dẫn đầy đủ về quan hệ Doctrine ORM trong Symfony. OneToMany, ManyToMany, chiến lược tải và tối ưu hiệu năng kèm ví dụ thực tế.

Symfony Live Components và UX 3.0: Ứng Dụng Phản Hồi Không Cần JavaScript Năm 2026
Symfony Live Components xây dựng giao diện phản hồi bằng PHP và Twig mà không cần JavaScript. Hướng dẫn chi tiết về LiveProp, LiveAction, form và deferred loading.

Symfony 7: API Platform va Cac Thuc Hanh Tot Nhat
Huong dan day du ve API Platform 4 voi Symfony 7. Tu State Processors, State Providers den bao mat va kiem thu — tat ca thuc hanh tot nhat cho REST API san xuat.