如何在API Platform中实现带过滤的自定义统计获取端点
解决Symfony/API Platform自定义用户统计端点的过滤问题
我来帮你搞定这个自定义统计端点的过滤需求,结合API Platform的特性,有两个比较靠谱的方案,我给你一步步拆解:
方案一:用自定义控制器(最直接易上手)
既然是单个用户的统计接口,自定义控制器可以直接拿到请求参数,逻辑实现起来更灵活。
1. 先定义统计结果的DTO(你的Statistic实体)
这个DTO不需要ORM映射,只用来定义返回的结构,记得加上API Platform的序列化配置:
// src/Dto/UserStatistics.php namespace App\Dto; use ApiPlatform\Metadata\ApiProperty; class UserStatistics { /** * @ApiProperty(openapiContext={"type"="string"}) */ public string $sport; /** * @ApiProperty(openapiContext={"type"="string", "nullable": true}) */ public ?string $position; /** * @ApiProperty(openapiContext={"type"="integer"}) */ public int $totalMatches; /** * @ApiProperty(openapiContext={"type"="integer"}) */ public int $winCount; /** * @ApiProperty(openapiContext={"type"="integer"}) */ public int $drawCount; /** * @ApiProperty(openapiContext={"type"="integer"}) */ public int $lossCount; }
2. 在User实体上配置自定义操作
给User添加一个statistics自定义操作,指定控制器并声明可接收的过滤参数:
// src/Entity/User.php use ApiPlatform\Metadata\ApiOperation; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\Get; use App\Controller\UserStatisticsController; // ... class User implements UserInterface { // ... #[Get( uriTemplate: '/users/{id}/statistics', controller: UserStatisticsController::class, openapiContext: [ 'parameters' => [ [ 'name' => 'sport[]', 'in' => 'query', 'required' => false, 'schema' => ['type' => 'array', 'items' => ['type' => 'string']], 'style' => 'form' ], [ 'name' => 'outcome', 'in' => 'query', 'required' => false, 'schema' => ['type' => 'string', 'enum' => ['win', 'draw', 'loss']] ] ] ] )] #[ApiProperty(readable: false)] // 避免这个属性被序列化到User的响应里 public function getStatistics(): array { // 这个方法只是占位,实际逻辑在控制器里 return []; } }
3. 编写自定义控制器逻辑
控制器里直接注入Request和EntityManager,拿到用户ID、过滤参数,然后用Doctrine QueryBuilder做统计查询:
// src/Controller/UserStatisticsController.php namespace App\Controller; use App\Entity\User; use App\Dto\UserStatistics; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Exception\AccessDeniedException; class UserStatisticsController { public function __invoke(User $user, Request $request, EntityManagerInterface $em): array { // 可选:权限控制,比如只允许用户查看自己的统计 // $currentUser = $this->getUser(); // if ($currentUser !== $user) { // throw new AccessDeniedException(); // } // 获取过滤参数 $sports = $request->query->all('sport'); $outcomeFilter = $request->query->get('outcome'); // 构建统计查询(这里假设Match的outcome字段:1=胜,2=平,3=负,你要改成自己的常量值) $qb = $em->createQueryBuilder() ->select([ 's.name as sport', 'p.name as position', 'COUNT(mp.id) as totalMatches', 'SUM(CASE WHEN m.outcome = 1 THEN 1 ELSE 0 END) as winCount', 'SUM(CASE WHEN m.outcome = 2 THEN 1 ELSE 0 END) as drawCount', 'SUM(CASE WHEN m.outcome = 3 THEN 1 ELSE 0 END) as lossCount' ]) ->from('App\Entity\MatchPlayer', 'mp') ->join('mp.user', 'u') ->join('mp.playedMatch', 'm') ->join('m.sport', 's') ->leftJoin('mp.position', 'p') ->where('u.id = :userId') ->setParameter('userId', $user->getId()) ->groupBy('s.id, p.id'); // 应用sport过滤 if (!empty($sports)) { $qb->andWhere('s.name IN (:sports)') ->setParameter('sports', $sports); } // 应用outcome过滤 if ($outcomeFilter) { $outcomeMap = [ 'win' => 1, 'draw' => 2, 'loss' => 3 ]; if (isset($outcomeMap[$outcomeFilter])) { $qb->andWhere('m.outcome = :outcome') ->setParameter('outcome', $outcomeMap[$outcomeFilter]); } } $results = $qb->getQuery()->getResult(); // 映射查询结果到DTO $statistics = []; foreach ($results as $row) { $stat = new UserStatistics(); $stat->sport = $row['sport']; $stat->position = $row['position']; $stat->totalMatches = (int)$row['totalMatches']; $stat->winCount = (int)$row['winCount']; $stat->drawCount = (int)$row['drawCount']; $stat->lossCount = (int)$row['lossCount']; $statistics[] = $stat; } return $statistics; } }
方案二:自定义数据提供者(更贴合API Platform规范)
如果你想复用API Platform的过滤系统,可以自定义数据提供者,从请求上下文里获取过滤参数:
1. 实现自定义数据提供者
// src/DataProvider/UserStatisticsDataProvider.php namespace App\DataProvider; use ApiPlatform\Core\DataProvider\ContextAwareDataProviderInterface; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use App\Entity\User; use App\Dto\UserStatistics; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RequestStack; class UserStatisticsDataProvider implements ContextAwareDataProviderInterface, RestrictedDataProviderInterface, ItemDataProviderInterface { public function __construct( private EntityManagerInterface $em, private RequestStack $requestStack ) {} public function supports(string $resourceClass, string $operationName = null, array $context = []): bool { // 只支持User的statistics操作 return $resourceClass === User::class && $operationName === 'get_statistics'; } public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): array { $user = $this->em->getRepository(User::class)->find($id); if (!$user) { return []; } $request = $this->requestStack->getCurrentRequest(); $sports = $request->query->all('sport'); $outcomeFilter = $request->query->get('outcome'); // 这里的查询逻辑和控制器里的完全一致,省略重复代码 $qb = $this->em->createQueryBuilder() // 同上的select、from、join、where、groupBy // 应用过滤条件 // ... $results = $qb->getQuery()->getResult(); // 映射到DTO,同上逻辑 $statistics = []; foreach ($results as $row) { $stat = new UserStatistics(); // 赋值属性... $statistics[] = $stat; } return $statistics; } public function getCollection(string $resourceClass, string $operationName = null, array $context = []): iterable { // 不需要实现,因为是单个用户的统计 throw new \LogicException('Not implemented'); } }
2. 在User的自定义操作里指定数据提供者
修改User实体的Get注解,去掉controller,加上provider:
#[Get( uriTemplate: '/users/{id}/statistics', provider: UserStatisticsDataProvider::class, openapiContext: [ // 同上的parameters配置 ] )]
关键注意点
- 参数映射:确保你的
outcome参数和Match实体里的$outcome字段值对应(比如示例中的1=胜,你要改成自己项目里的常量) - 权限控制:一定要加上权限校验,避免用户查看其他用户的统计数据
- 缓存优化:统计数据可以用Symfony Cache组件缓存,减少数据库查询压力
- 序列化配置:给DTO添加正确的序列化组,确保返回的JSON结构符合预期
内容的提问来源于stack exchange,提问作者tobias ingold
相关产品推荐
相关产品推荐

