You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在API Platform自定义状态提供器中添加HTTP自定义头的最优方案

问题描述

我在API Platform中用自定义状态提供器(CustomStateProvider)包装返回DTO(Representation)来实现字段隐藏这类需求,这种思路我很认可。目前我用标准的ApiPlatform\Doctrine\Orm\State\CollectionProvider处理筛选、分页逻辑,代码如下:

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\Doctrine\Orm\State\CollectionProvider;
use Doctrine\Persistence\ManagerRegistry;
use App\Dto\ClientRepresentation;

class ClientCollectionProvider implements ProviderInterface
{
    private CollectionProvider $collectionProvider;
    public function __construct(CollectionProvider $collectionProvider)
    {
        $this->collectionProvider = $collectionProvider;
    }

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        $origResult = $this->collectionProvider->provide($operation, $uriVariables, $context);
        header("Pagination-Pages: ".$origResult->getLastPage());
        header("Pagination-Count: ".$origResult->getTotalItems());
        header("Pagination-Limit: ".$origResult->getItemsPerPage());
        return array_map(
            fn($client): ClientRepresentation => ClientRepresentation::fromClient($client),
            iterator_to_array($origResult->getIterator())
        );
    }

}

如代码所示,我用PHP原生header()函数加了几个分页相关的自定义响应头:Pagination-Pages(总页数)、Pagination-Count(总条目数)、Pagination-Limit(每页最大条目数)。这个方案能跑通,但总觉得不够优雅。想知道在API Platform的自定义状态提供器里,有没有更规范的添加自定义HTTP头的方案?我了解过实现EventSubscriberInterface的方式,但感觉和直接用header()函数一样不够理想。


更优雅的解决方案

方案一:直接返回Symfony\Component\HttpFoundation\Response对象

这是最贴合Symfony和API Platform规范的做法,完全替代原生header()函数:

  1. 注入Symfony的SerializerInterface用于序列化DTO数组
  2. 构造Response对象,直接在里面设置响应内容和自定义头

修改后的代码示例:

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\Doctrine\Orm\State\CollectionProvider;
use App\Dto\ClientRepresentation;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\SerializerInterface;

class ClientCollectionProvider implements ProviderInterface
{
    private CollectionProvider $collectionProvider;
    private SerializerInterface $serializer;

    public function __construct(CollectionProvider $collectionProvider, SerializerInterface $serializer)
    {
        $this->collectionProvider = $collectionProvider;
        $this->serializer = $serializer;
    }

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null|Response
    {
        $origResult = $this->collectionProvider->provide($operation, $uriVariables, $context);
        
        // 转换为DTO数组
        $dtoCollection = array_map(
            fn($client): ClientRepresentation => ClientRepresentation::fromClient($client),
            iterator_to_array($origResult->getIterator())
        );

        // 序列化DTO数组,保留API Platform的序列化上下文
        $content = $this->serializer->serialize($dtoCollection, 'json', [
            'operation' => $operation,
            'uri_variables' => $uriVariables,
            'context' => $context,
        ]);

        // 创建Response并设置自定义头
        $response = new Response($content, Response::HTTP_OK, [
            'Content-Type' => 'application/json',
            'Pagination-Pages' => $origResult->getLastPage(),
            'Pagination-Count' => $origResult->getTotalItems(),
            'Pagination-Limit' => $origResult->getItemsPerPage(),
        ]);

        return $response;
    }
}

优势:

  • 完全遵循Symfony的HTTP响应规范,避免直接操作全局头
  • 可灵活控制响应状态码、内容类型等细节
  • 与API Platform的序列化系统深度集成,保留所有序列化逻辑

方案二:Context传参+事件订阅器(解耦版)

如果不想直接返回Response,可以把分页数据存入状态提供器的$context,再通过事件订阅器统一设置响应头,实现职责分离:

  1. 在状态提供器中把分页数据存入$context:
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
    $origResult = $this->collectionProvider->provide($operation, $uriVariables, $context);
    
    // 将分页信息存入context,供后续事件使用
    $context['pagination_metadata'] = [
        'pages' => $origResult->getLastPage(),
        'count' => $origResult->getTotalItems(),
        'limit' => $origResult->getItemsPerPage(),
    ];

    return array_map(
        fn($client): ClientRepresentation => ClientRepresentation::fromClient($client),
        iterator_to_array($origResult->getIterator())
    );
}
  1. 编写事件订阅器,从$context取数据设置响应头:
namespace App\EventSubscriber;

use ApiPlatform\Core\EventListener\EventPriorities;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class PaginationHeaderSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::VIEW => ['onView', EventPriorities::POST_SERIALIZE],
        ];
    }

    public function onView(ViewEvent $event): void
    {
        $request = $event->getRequest();
        $context = $request->attributes->get('_api_context', []);
        
        if (!isset($context['pagination_metadata'])) {
            return;
        }

        $response = $event->getResponse();
        $metadata = $context['pagination_metadata'];
        
        $response->headers->set('Pagination-Pages', $metadata['pages']);
        $response->headers->set('Pagination-Count', $metadata['count']);
        $response->headers->set('Pagination-Limit', $metadata['limit']);
    }
}

优势:

  • 状态提供器只负责数据处理,响应头设置由专门的订阅器处理,职责清晰
  • 订阅器可复用,支持多个资源的分页头需求

方案三:自定义API Platform分页扩展(框架原生风格)

如果分页逻辑是通用的,可以自定义API Platform的分页扩展,自动注入分页头,最贴合框架设计理念:

namespace App\ApiPlatform\Extension;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
use ApiPlatform\Core\Metadata\Operation;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CustomPaginationExtension implements QueryResultExtensionInterface, EventSubscriberInterface
{
    private ?int $totalItems = null;
    private ?int $lastPage = null;
    private ?int $itemsPerPage = null;

    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
    {
        // 计算总条目数
        $countQueryBuilder = clone $queryBuilder;
        $countQueryBuilder->select('COUNT(DISTINCT o.id)')->setMaxResults(1);
        $this->totalItems = (int) $countQueryBuilder->getQuery()->getSingleScalarResult();
        
        // 获取每页条目数,默认30
        $this->itemsPerPage = $context['pagination_items_per_page'] ?? 30;
        $this->lastPage = (int) ceil($this->totalItems / $this->itemsPerPage);
    }

    public function supportsResult(string $resourceClass, Operation $operation = null, array $context = []): bool
    {
        return true; // 可根据需要限制适用的资源类
    }

    public function getResult(QueryBuilder $queryBuilder, Operation $operation = null, array $context = []): ?array
    {
        return $queryBuilder->getQuery()->getResult();
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::VIEW => ['onView', EventPriorities::POST_SERIALIZE],
        ];
    }

    public function onView(ViewEvent $event): void
    {
        if (null === $this->totalItems) {
            return;
        }

        $response = $event->getResponse();
        $response->headers->set('Pagination-Pages', $this->lastPage);
        $response->headers->set('Pagination-Count', $this->totalItems);
        $response->headers->set('Pagination-Limit', $this->itemsPerPage);
        
        // 重置属性,避免请求间数据污染
        $this->totalItems = null;
        $this->lastPage = null;
        $this->itemsPerPage = null;
    }
}

优势:

  • 完全融入API Platform的扩展体系,无需修改状态提供器
  • 通用化程度高,可快速应用到多个资源

内容的提问来源于stack exchange,提问作者MisterFungus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 19:03:28