Symfony4 API CRUD通用父控制器实现示例请求
Awesome move extracting repetitive CRUD logic into a base controller—this will save you tons of time as your API grows. Let’s create a flexible BaseCrudController that your ArticleController (and future resource controllers) can extend, while keeping room for customizations when needed.
Step 1: Create the Abstract Base CRUD Controller
First, we’ll build an abstract base controller with generic CRUD actions. We’ll use abstract methods to let child controllers define entity-specific details like the entity class, validation groups, and route names.
<?php namespace App\Controller; use App\Exception\ResourceValidationException; use FOS\RestBundle\Controller\FOSRestController; use FOS\RestBundle\Request\ParamFetcherInterface; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Validator\ConstraintViolationList; abstract class BaseCrudController extends FOSRestController { /** * Return the fully qualified class name of the entity this controller manages */ abstract protected function getEntityClass(): string; /** * Return validation groups for create action */ abstract protected function getCreateValidationGroups(): array; /** * Return the route name for the show action (used to generate Location header) */ abstract protected function getShowRouteName(): string; /** * Generic show action */ public function showAction($entity) { return $entity; } /** * Generic create action * @ParamConverter( * "entity", converter="fos_rest.request_body", * options={ "validator"={ "groups"=::getCreateValidationGroups()} } * ) */ public function createAction($entity, ConstraintViolationList $violations) { if (count($violations) > 0) { $message = 'The JSON sent contains invalid data: '; foreach ($violations as $violation) { $message .= sprintf("Field %s: %s; ", $violation->getPropertyPath(), $violation->getMessage()); } throw new ResourceValidationException(rtrim($message, "; ")); } $em = $this->getDoctrine()->getManager(); $em->persist($entity); $em->flush(); return $this->view( $entity, Response::HTTP_CREATED, [ 'Location' => $this->generateUrl( $this->getShowRouteName(), ['id' => $entity->getId()], UrlGeneratorInterface::ABSOLUTE_URL ) ] ); } /** * Generic list action with pagination and search */ public function listAction(ParamFetcherInterface $paramFetcher) { $repository = $this->getDoctrine()->getRepository($this->getEntityClass()); // Assume your repositories implement a `search` method with these parameters $pager = $repository->search( $paramFetcher->get('keyword'), $paramFetcher->get('order'), $paramFetcher->get('limit'), $paramFetcher->get('offset') ); // Dynamically load your custom Representation class $entityShortName = $this->getEntityShortName(); $representationClass = "App\Representation\\{$entityShortName}s"; return new $representationClass($pager); } /** * Helper to get the short entity name (e.g., "Article" from "App\Entity\Article") */ private function getEntityShortName(): string { $classParts = explode('\\', $this->getEntityClass()); return end($classParts); } }
Step 2: Simplify Your ArticleController
Now, update your ArticleController to inherit the base controller and implement the required abstract methods. You can keep custom logic if needed, but most actions will be handled by the parent.
<?php namespace App\Controller; use App\Entity\Article; use FOS\RestBundle\Controller\Annotations as Rest; use Nelmio\ApiDocBundle\Annotation\Security; use Swagger\Annotations as SWG; use Symfony\Component\Validator\ConstraintViolationList; use FOS\RestBundle\Request\ParamFetcherInterface; class ArticleController extends BaseCrudController { /** * @inheritDoc */ protected function getEntityClass(): string { return Article::class; } /** * @inheritDoc */ protected function getCreateValidationGroups(): array { return ['Create']; } /** * @inheritDoc */ protected function getShowRouteName(): string { return 'app_article_show'; } /** * @Rest\Get( * path="/api/articles/{id}", * name="app_article_show", * requirements={"id"="\d+"} * ) * @Rest\View() * @SWG\Response( * response=200, * description="Return article." * ) * @SWG\Tag(name="Article") * @Security(name="Bearer") */ public function showAction(Article $article) { // Call parent method, or add custom logic here if needed return parent::showAction($article); } /** * @Rest\Post( * path="/api/articles", * name="app_article_create" * ) * @Rest\View(StatusCode=201) * @SWG\Response( * response=201, * description="Create article" * ) * @SWG\Tag(name="Article") * @Security(name="Bearer") */ public function createAction(Article $article, ConstraintViolationList $violations) { return parent::createAction($article, $violations); } /** * @Rest\Get( * path="/api/articles", * name="app_article_list" * ) * @Rest\QueryParam( * name="keyword", * requirements="[a-zA-Z0-9]", * nullable=true, * description="The keyword to search for." * ) * @Rest\QueryParam( * name="order", * requirements="asc|desc", * default="asc", * description="Sort order (asc or desc)" * ) * @Rest\QueryParam( * name="limit", * requirements="\d+", * default="16", * description="Max number of articles per page." * ) * @Rest\QueryParam( * name="offset", * requirements="\d+", * default="1", * description="The pagination offset" * ) * @Rest\View() * @SWG\Response( * response=200, * description="List all articles." * ) * @SWG\Tag(name="Article") * @Security(name="Bearer") */ public function listAction(ParamFetcherInterface $paramFetcher) { // Add custom Article-specific logic here if needed return parent::listAction($paramFetcher); } }
Key Notes & Customizations
- Abstract Methods: These enforce entity-specific configuration, keeping the base controller flexible for any resource.
- Custom Logic: If you need to override a base action (e.g., add extra validation for Article creation), just redefine the method in
ArticleControllerand modify it as needed. - Repository Consistency: The base controller assumes your repositories have a
searchmethod with the same signature. To make this more generic, you could add an abstract methodgetSearchQueryBuilder()or use a trait for shared repository logic. - Dependency Injection: For Symfony 4+, consider using constructor injection for
EntityManagerInterfacein the base controller instead of$this->getDoctrine()—it’s more testable and aligns with Symfony’s best practices.
内容的提问来源于stack exchange,提问作者Sha91

