控制器高级 #

一、参数转换器 #

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