API开发 #

一、API概述 #

1.1 RESTful API #

text
┌─────────────────────────────────────────────────────┐
│                  RESTful API设计                    │
├─────────────────────────────────────────────────────┤
│                                                     │
│   GET    /api/users       获取用户列表              │
│   POST   /api/users       创建用户                  │
│   GET    /api/users/{id}  获取单个用户              │
│   PUT    /api/users/{id}  更新用户                  │
│   DELETE /api/users/{id}  删除用户                  │
│                                                     │
└─────────────────────────────────────────────────────┘

1.2 安装API组件 #

bash
# 安装序列化组件
composer require serializer

# 安装API Platform(可选)
composer require api

二、API控制器 #

2.1 基本API控制器 #

php
<?php

namespace App\Controller\Api;

use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;

#[Route('/api')]
class UserApiController extends AbstractController
{
    public function __construct(
        private SerializerInterface $serializer
    ) {}

    #[Route('/users', name: 'api_users_list', methods: ['GET'])]
    public function list(UserRepository $userRepository): JsonResponse
    {
        $users = $userRepository->findAll();
        
        $json = $this->serializer->serialize($users, 'json', [
            'groups' => 'user:read',
        ]);

        return JsonResponse::fromJsonString($json);
    }

    #[Route('/users/{id}', name: 'api_users_show', methods: ['GET'])]
    public function show(User $user): JsonResponse
    {
        $json = $this->serializer->serialize($user, 'json', [
            'groups' => 'user:read',
        ]);

        return JsonResponse::fromJsonString($json);
    }

    #[Route('/users', name: 'api_users_create', methods: ['POST'])]
    public function create(Request $request, EntityManagerInterface $entityManager): JsonResponse
    {
        $user = $this->serializer->deserialize(
            $request->getContent(),
            User::class,
            'json',
            ['groups' => 'user:write']
        );

        $entityManager->persist($user);
        $entityManager->flush();

        $json = $this->serializer->serialize($user, 'json', [
            'groups' => 'user:read',
        ]);

        return JsonResponse::fromJsonString($json, Response::HTTP_CREATED);
    }

    #[Route('/users/{id}', name: 'api_users_update', methods: ['PUT', 'PATCH'])]
    public function update(
        Request $request,
        User $user,
        EntityManagerInterface $entityManager
    ): JsonResponse {
        $this->serializer->deserialize(
            $request->getContent(),
            User::class,
            'json',
            [
                'groups' => 'user:write',
                'object_to_populate' => $user,
            ]
        );

        $entityManager->flush();

        $json = $this->serializer->serialize($user, 'json', [
            'groups' => 'user:read',
        ]);

        return JsonResponse::fromJsonString($json);
    }

    #[Route('/users/{id}', name: 'api_users_delete', methods: ['DELETE'])]
    public function delete(User $user, EntityManagerInterface $entityManager): JsonResponse
    {
        $entityManager->remove($user);
        $entityManager->flush();

        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
    }
}

三、序列化 #

3.1 实体序列化配置 #

php
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;

#[ORM\Entity]
class User
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    #[Groups(['user:read'])]
    private ?int $id = null;

    #[ORM\Column(length: 100)]
    #[Groups(['user:read', 'user:write'])]
    private ?string $name = null;

    #[ORM\Column(length: 180, unique: true)]
    #[Groups(['user:read', 'user:write'])]
    private ?string $email = null;

    #[ORM\Column]
    #[Groups(['user:write'])]
    private ?string $password = null;

    #[ORM\Column]
    #[Groups(['user:read'])]
    private ?\DateTimeImmutable $createdAt = null;

    #[SerializedName('fullName')]
    #[Groups(['user:read'])]
    public function getFullName(): string
    {
        return $this->name;
    }
}

3.2 序列化上下文 #

php
<?php

$json = $this->serializer->serialize($user, 'json', [
    'groups' => ['user:read'],
    'datetime_format' => 'Y-m-d H:i:s',
    'enable_max_depth' => true,
    'circular_reference_handler' => function ($object) {
        return $object->getId();
    },
]);

四、验证 #

4.1 API验证 #

php
<?php

namespace App\Controller\Api;

use Symfony\Component\Validator\Validator\ValidatorInterface;

class UserApiController extends AbstractController
{
    public function create(
        Request $request,
        EntityManagerInterface $entityManager,
        ValidatorInterface $validator
    ): JsonResponse {
        $user = $this->serializer->deserialize(
            $request->getContent(),
            User::class,
            'json'
        );

        $errors = $validator->validate($user);

        if (count($errors) > 0) {
            $errorMessages = [];
            foreach ($errors as $error) {
                $errorMessages[$error->getPropertyPath()] = $error->getMessage();
            }

            return $this->json([
                'errors' => $errorMessages,
            ], Response::HTTP_BAD_REQUEST);
        }

        $entityManager->persist($user);
        $entityManager->flush();

        return $this->json($user, Response::HTTP_CREATED, [], [
            'groups' => 'user:read',
        ]);
    }
}

五、分页 #

5.1 分页实现 #

php
<?php

namespace App\Controller\Api;

use App\Repository\UserRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

class UserApiController extends AbstractController
{
    #[Route('/users', name: 'api_users_list', methods: ['GET'])]
    public function list(Request $request, UserRepository $userRepository): JsonResponse
    {
        $page = $request->query->getInt('page', 1);
        $limit = $request->query->getInt('limit', 10);
        $offset = ($page - 1) * $limit;

        $users = $userRepository->findBy([], ['createdAt' => 'DESC'], $limit, $offset);
        $total = $userRepository->count([]);

        return $this->json([
            'data' => $users,
            'meta' => [
                'total' => $total,
                'page' => $page,
                'limit' => $limit,
                'pages' => ceil($total / $limit),
            ],
        ], Response::HTTP_OK, [], ['groups' => 'user:read']);
    }
}

六、错误处理 #

6.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 ApiExceptionSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::EXCEPTION => 'onKernelException',
        ];
    }

    public function onKernelException(ExceptionEvent $event): void
    {
        $request = $event->getRequest();
        
        if (!str_starts_with($request->getPathInfo(), '/api/')) {
            return;
        }

        $exception = $event->getThrowable();

        $statusCode = $exception instanceof HttpException
            ? $exception->getStatusCode()
            : Response::HTTP_INTERNAL_SERVER_ERROR;

        $data = [
            'error' => [
                'code' => $statusCode,
                'message' => $exception->getMessage(),
            ],
        ];

        if ($statusCode === Response::HTTP_NOT_FOUND) {
            $data['error']['message'] = 'Resource not found';
        }

        $event->setResponse(new JsonResponse($data, $statusCode));
    }
}

七、API认证 #

7.1 API Token认证 #

php
<?php

namespace App\Security;

use App\Repository\UserRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
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 ApiTokenAuthenticator extends AbstractAuthenticator
{
    public function __construct(
        private UserRepository $userRepository
    ) {}

    public function supports(Request $request): ?bool
    {
        return $request->headers->has('X-API-Token');
    }

    public function authenticate(Request $request): Passport
    {
        $apiToken = $request->headers->get('X-API-Token');

        return new SelfValidatingPassport(
            new UserBadge($apiToken, function (string $apiToken) {
                return $this->userRepository->findOneBy(['apiToken' => $apiToken]);
            })
        );
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        return new JsonResponse([
            'error' => 'Authentication failed',
        ], Response::HTTP_UNAUTHORIZED);
    }
}

八、CORS配置 #

8.1 NelmioCorsBundle #

bash
composer require nelmio/cors-bundle
yaml
# config/packages/nelmio_cors.yaml
nelmio_cors:
    defaults:
        origin_regex: true
        allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
        allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
        allow_headers: ['Content-Type', 'Authorization']
        expose_headers: ['Link']
        max_age: 3600
    paths:
        '^/api/':
            allow_origin: ['*']
            allow_methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
            allow_headers: ['Content-Type', 'Authorization']
            max_age: 3600

九、API文档 #

9.1 Swagger/OpenAPI #

bash
composer require nelmio/api-doc-bundle
composer require twig
yaml
# config/packages/nelmio_api_doc.yaml
nelmio_api_doc:
    areas:
        path_patterns:
            - ^/api
    documentation:
        info:
            title: My API
            description: API文档
            version: 1.0.0
        components:
            securitySchemes:
                Bearer:
                    type: http
                    scheme: bearer
                    bearerFormat: JWT
        security:
            - Bearer: []

9.2 添加文档注解 #

php
<?php

use OpenApi\Attributes as OA;

#[OA\Tag(name: 'Users')]
class UserApiController extends AbstractController
{
    #[OA\Get(
        path: '/api/users',
        summary: '获取用户列表',
        tags: ['Users'],
        responses: [
            new OA\Response(
                response: 200,
                description: '成功',
                content: new OA\JsonContent(
                    type: 'array',
                    items: new OA\Items(ref: '#/components/schemas/User')
                )
            )
        ]
    )]
    #[Route('/users', name: 'api_users_list', methods: ['GET'])]
    public function list(): JsonResponse
    {
    }
}

十、总结 #

本章学习了:

  • RESTful API设计
  • API控制器创建
  • 序列化配置
  • API验证
  • 分页实现
  • 错误处理
  • API认证
  • CORS配置
  • API文档生成

下一章将学习 博客系统实战

最后更新:2026-03-28