控制器高级 #
一、参数转换器 #
1.1 自动实体转换 #
php
<?php
namespace App\Controller;
use App\Entity\User;
use App\Entity\Article;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class UserController extends AbstractController
{
#[Route('/user/{id}', name: 'app_user_show')]
public function show(User $user): Response
{
return $this->render('user/show.html.twig', [
'user' => $user,
]);
}
#[Route('/article/{slug}', name: 'app_article_show')]
public function articleShow(Article $article): Response
{
return $this->render('article/show.html.twig', [
'article' => $article,
]);
}
}
1.2 ParamConverter配置 #
php
<?php
use App\Entity\Category;
use App\Entity\Product;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
class ProductController extends AbstractController
{
#[Route('/category/{category_id}/product/{product_id}', name: 'app_product_show')]
#[ParamConverter('category', options: ['id' => 'category_id'])]
#[ParamConverter('product', options: ['id' => 'product_id'])]
public function show(Category $category, Product $product): Response
{
return $this->render('product/show.html.twig', [
'category' => $category,
'product' => $product,
]);
}
#[Route('/blog/{slug}', name: 'app_blog_show')]
#[ParamConverter('post', class: 'App\Entity\Post', options: ['mapping' => ['slug' => 'slug']])]
public function blogShow(Post $post): Response
{
return $this->render('blog/show.html.twig', [
'post' => $post,
]);
}
}
1.3 自定义参数转换器 #
php
<?php
namespace App\ParamConverter;
use App\Entity\User;
use App\Repository\UserRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class UserParamConverter implements ParamConverterInterface
{
public function __construct(
private UserRepository $userRepository
) {}
public function apply(Request $request, ParamConverter $configuration): bool
{
$userId = $request->attributes->get('user_id');
$user = $this->userRepository->find($userId);
if (!$user) {
throw new NotFoundHttpException('User not found');
}
$request->attributes->set($configuration->getName(), $user);
return true;
}
public function supports(ParamConverter $configuration): bool
{
return $configuration->getClass() === User::class;
}
}
二、控制器事件 #
2.1 请求事件 #
php
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class RequestSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => [
['setLocale', 20],
['validateToken', 10],
],
];
}
public function setLocale(RequestEvent $event): void
{
$request = $event->getRequest();
if ($request->query->has('_locale')) {
$request->setLocale($request->query->get('_locale'));
}
}
public function validateToken(RequestEvent $event): void
{
$request = $event->getRequest();
if (str_starts_with($request->getPathInfo(), '/api/')) {
$token = $request->headers->get('X-API-Token');
if (!$token || !$this->isValidToken($token)) {
throw new \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException('Invalid token');
}
}
}
private function isValidToken(string $token): bool
{
return $token === 'valid-token';
}
}
2.2 控制器事件 #
php
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class ControllerSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER => 'onKernelController',
];
}
public function onKernelController(ControllerEvent $event): void
{
$controller = $event->getController();
$request = $event->getRequest();
if (is_array($controller)) {
$controllerObject = $controller[0];
$method = $controller[1];
if (method_exists($controllerObject, 'setRequest')) {
$controllerObject->setRequest($request);
}
}
}
}
2.3 响应事件 #
php
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class ResponseSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::RESPONSE => [
['addSecurityHeaders', 0],
['addCorsHeaders', 0],
],
];
}
public function addSecurityHeaders(ResponseEvent $event): void
{
$response = $event->getResponse();
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-XSS-Protection', '1; mode=block');
}
public function addCorsHeaders(ResponseEvent $event): void
{
$response = $event->getResponse();
$request = $event->getRequest();
if ($request->headers->has('Origin')) {
$response->headers->set('Access-Control-Allow-Origin', '*');
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
}
}
三、异常处理 #
3.1 异常监听器 #
php
<?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\HttpException;
use Symfony\Component\HttpKernel\KernelEvents;
class ExceptionSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::EXCEPTION => 'onKernelException',
];
}
public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
$request = $event->getRequest();
if (str_starts_with($request->getPathInfo(), '/api/')) {
$this->handleApiException($event, $exception);
}
}
private function handleApiException(ExceptionEvent $event, \Throwable $exception): void
{
$statusCode = $exception instanceof HttpException
? $exception->getStatusCode()
: 500;
$data = [
'error' => [
'code' => $statusCode,
'message' => $exception->getMessage(),
],
];
$event->setResponse(new JsonResponse($data, $statusCode));
}
}
3.2 自定义异常页面 #
php
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
class ErrorController extends AbstractController
{
public function show(\Throwable $exception): Response
{
$statusCode = $exception instanceof HttpException
? $exception->getStatusCode()
: 500;
return $this->render('error/show.html.twig', [
'status_code' => $statusCode,
'message' => $exception->getMessage(),
]);
}
}
3.3 错误模板 #
twig
{# templates/error/show.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}Error {{ status_code }}{% endblock %}
{% block body %}
<div class="error-page">
<h1>Error {{ status_code }}</h1>
{% if status_code == 404 %}
<p>Page not found</p>
{% elseif status_code == 403 %}
<p>Access denied</p>
{% elseif status_code == 500 %}
<p>Internal server error</p>
{% else %}
<p>{{ message }}</p>
{% endif %}
<a href="{{ path('app_home') }}">Go to Home</a>
</div>
{% endblock %}
四、控制器服务 #
4.1 服务注入 #
php
<?php
namespace App\Controller;
use App\Service\UserService;
use App\Service\EmailService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class UserController extends AbstractController
{
public function __construct(
private UserService $userService,
private EmailService $emailService,
private EntityManagerInterface $entityManager,
private LoggerInterface $logger
) {}
#[Route('/users', name: 'app_user_list')]
public function list(): Response
{
$users = $this->userService->getAllUsers();
$this->logger->info('User list accessed');
return $this->render('user/list.html.twig', [
'users' => $users,
]);
}
#[Route('/user/create', name: 'app_user_create', methods: ['POST'])]
public function create(): Response
{
$user = $this->userService->createUser([
'name' => 'John Doe',
'email' => 'john@example.com',
]);
$this->emailService->sendWelcomeEmail($user);
$this->logger->info('User created', ['id' => $user->getId()]);
return $this->redirectToRoute('app_user_show', ['id' => $user->getId()]);
}
}
4.2 方法注入 #
php
<?php
class OrderController extends AbstractController
{
#[Route('/order/{id}', name: 'app_order_show')]
public function show(
int $id,
OrderService $orderService,
LoggerInterface $logger
): Response {
$order = $orderService->getOrderById($id);
$logger->info('Order viewed', ['order_id' => $id]);
return $this->render('order/show.html.twig', [
'order' => $order,
]);
}
}
4.3 服务定位器 #
php
<?php
use Symfony\Contracts\Service\ServiceSubscriberInterface;
class SmartController extends AbstractController implements ServiceSubscriberInterface
{
public static function getSubscribedServices(): array
{
return [
'App\Service\UserService',
'App\Service\OrderService',
'logger' => '?Psr\Log\LoggerInterface',
];
}
#[Route('/smart/users', name: 'app_smart_users')]
public function users(): Response
{
$userService = $this->container->get('App\Service\UserService');
$users = $userService->getAllUsers();
return $this->json($users);
}
}
五、控制器属性 #
5.1 自定义属性 #
php
<?php
namespace App\Attribute;
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
class RequireRole
{
public function __construct(
public string $role
) {}
}
5.2 使用自定义属性 #
php
<?php
namespace App\Controller;
use App\Attribute\RequireRole;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[RequireRole('ROLE_ADMIN')]
class AdminController extends AbstractController
{
#[Route('/admin/dashboard', name: 'app_admin_dashboard')]
public function dashboard(): Response
{
return $this->render('admin/dashboard.html.twig');
}
#[Route('/admin/users', name: 'app_admin_users')]
#[RequireRole('ROLE_SUPER_ADMIN')]
public function users(): Response
{
return $this->render('admin/users.html.twig');
}
}
5.3 属性处理器 #
php
<?php
namespace App\EventSubscriber;
use App\Attribute\RequireRole;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class RequireRoleSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER => 'onKernelController',
];
}
public function onKernelController(ControllerEvent $event): void
{
$attributes = [];
$reflectionClass = new \ReflectionClass($event->getControllerClass());
$classAttributes = $reflectionClass->getAttributes(RequireRole::class);
$attributes = array_merge($attributes, $classAttributes);
$reflectionMethod = $reflectionClass->getMethod($event->getRequest()->attributes->get('_route_params')['_controller_method'] ?? '__invoke');
$methodAttributes = $reflectionMethod->getAttributes(RequireRole::class);
$attributes = array_merge($attributes, $methodAttributes);
foreach ($attributes as $attribute) {
$requireRole = $attribute->newInstance();
$this->checkRole($requireRole->role);
}
}
private function checkRole(string $role): void
{
if (!$this->isGranted($role)) {
throw new AccessDeniedException("需要 {$role} 权限");
}
}
}
六、控制器最佳实践 #
6.1 瘦控制器原则 #
php
<?php
// 好的做法:控制器只协调
class OrderController extends AbstractController
{
public function __construct(
private OrderService $orderService
) {}
#[Route('/orders', name: 'app_order_list')]
public function list(): Response
{
$orders = $this->orderService->getOrders();
return $this->render('order/list.html.twig', [
'orders' => $orders,
]);
}
#[Route('/order/{id}', name: 'app_order_show')]
public function show(int $id): Response
{
$order = $this->orderService->getOrderById($id);
if (!$order) {
throw $this->createNotFoundException('订单不存在');
}
return $this->render('order/show.html.twig', [
'order' => $order,
]);
}
}
6.2 单一职责 #
php
<?php
// 每个控制器负责一个资源
class UserController extends AbstractController
{
#[Route('/users', name: 'app_user_list')]
public function list(): Response {}
#[Route('/user/{id}', name: 'app_user_show')]
public function show(int $id): Response {}
#[Route('/user/new', name: 'app_user_new')]
public function new(): Response {}
#[Route('/user/{id}/edit', name: 'app_user_edit')]
public function edit(int $id): Response {}
}
// API控制器单独管理
class UserApiController extends AbstractController
{
#[Route('/api/users', name: 'api_user_list', methods: ['GET'])]
public function list(): JsonResponse {}
#[Route('/api/users', name: 'api_user_create', methods: ['POST'])]
public function create(): JsonResponse {}
}
七、总结 #
本章学习了:
- 参数转换器使用
- 自定义参数转换器
- 控制器事件监听
- 异常处理机制
- 服务注入方式
- 自定义控制器属性
- 控制器最佳实践
下一章将学习 Twig基础。
最后更新:2026-03-28