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