Api 平台需要过滤器
Filter required in Api Platform
我使用 API 平台并且我在 https://api-platform.com/docs/core/filters/#creating-custom-filters
之后定义了一个自定义过滤器
它工作正常,但是我需要每次应用程序对特定实体(设置过滤器的地方)执行 GET HTTP 请求时都需要过滤器。
我检查过这段代码:
// This function is only used to hook in documentation generators (supported by Swagger and Hydra)
public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}
$description = [];
foreach ($this->properties as $property => $strategy) {
$description["similar_$property"] = [
'property' => $property,
'type' => 'string',
'required' => false,
'swagger' => [
'description' => 'Filter using a similar postgres function.',
'name' => $property,
'type' => 'string',
],
];
}
return $description;
}
虽然 getDescription 有必填字段,但它仅适用于 api 文档而不适用于 HTTP 请求
您可以通过事件系统强制执行过滤器。
/**
* @ApiResource
*/
class Resource {
...
}
<?php
namespace App\EventSubscriber;
use ApiPlatform\Core\EventListener\EventPriorities;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
class ResourceReadSubscriber implements EventSubscriberInterface
{
/**
* @return array The event names to listen to
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::CONTROLLER => [
['hasFilter', EventPriorities::PRE_READ],
],
];
}
public function hasFilter(ControllerEvent $event)
{
// first check if this affects the requested resource
$resource = $event->getRequest()->attributes->get('_api_resource_class');
if (Resource::class !== $resource) {
return;
}
// second check if this is the get_collection controller
$controller = $event->getRequest()->attributes->get('_controller');
if ('api_platform.action.get_collection' !== $controller) {
return;
}
// third validate the required filter is set
// we expect a filter via GET parameter 'filter-query-parameter'
if (!$event->getRequest()->query->has('filter-query-parameter')) {
throw new BadRequestHttpException('Filter is required');
}
}
}
我使用自定义数据提供程序实施了过滤器。
在自动连接和自动配置都设置为 true 的情况下,我将其添加到 App\DataProvider:
<?php
namespace App\DataProvider;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\FilterExtension;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface;
use Doctrine\Common\Persistence\ManagerRegistry;
use ApiPlatform\Core\Exception\RuntimeException;
use App\Entity\Activity;
final class EventRequireCollectionDataProvider implements CollectionDataProviderInterface, RestrictedDataProviderInterface
{
private $collectionExtensions;
private $managerRegistry;
public function __construct(ManagerRegistry $managerRegistry, iterable $collectionExtensions)
{
$this->managerRegistry = $managerRegistry;
$this->collectionExtensions = $collectionExtensions;
}
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Activity::class === $resourceClass;
}
/**
* {@inheritdoc}
*
* @throws RuntimeException
*/
public function getCollection(string $resourceClass, string $operationName = null, array $context = [])
{
//This is where the filter is enforced...
if (empty($context) || !array_key_exists ( "filters" , $context ) || !array_key_exists ( "event" , $context['filters'] ) || !ctype_digit($context['filters']['event'])){
throw new RuntimeException('The api call must have the event id parameter set.');
}
$manager = $this->managerRegistry->getManagerForClass($resourceClass);
$repository = $manager->getRepository($resourceClass);
$queryBuilder = $repository->createQueryBuilder('o');
$queryNameGenerator = new QueryNameGenerator();
//This goes through all the built in extensions Api Platform provides.
//The filter is automatically added to the query here
foreach ($this->collectionExtensions as $extension) {
$extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName)){
$result = $extension->getResult($queryBuilder, $resourceClass, $operationName, $context);
return $result;
}
}
$queryBuilder->getQuery()->getResult();
}
}
我的实体(重要的东西 - 我的实体也使用组标准化器):
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
use ApiPlatform\Core\Action\NotFoundAction;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use App\Controller\DoNothingController;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ApiResource(
* iri="http://schema.org/Event",
* collectionOperations={
* "get" = {
* "normalization_context"={"groups"={"event_activities"}},
* "swagger_context"={
* "summary" = "Retrieves the activities for a specific event.",
* "description" = "This operation will obtain all the activities (i.e., the schedule) for a particular event. Currently, the event parameter is required and only one can be submitted at a time. Otherwise, an error will occur."
* },
* }
* },
* itemOperations={
* "get" = {
* "controller"=NotFoundAction::class,
* "read"=false,
* "output"=false,
* }
* },
* )
* @ApiFilter(SearchFilter::class, properties={"event": "exact"})
* @ORM\Entity(repositoryClass="App\Repository\ActivityRepository")
*/
class Activity
{
/**
* @var integer The activity identifier
*
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
* @Groups({"event_activities"})
*/
private $id;
/**
* @var \DateTime The start time for the activity.
* @ORM\Column(type="datetimetz", nullable=true)
* @Groups({"event_activities"})
*/
private $startTime;
/**
* @var \DateTime The ending time for the activity.
* @ORM\Column(type="datetimetz", nullable=true)
* @Groups({"event_activities"})
* @ApiProperty(iri="http://schema.org/endDate")
*/
private $endTime;
/**
* @var \DateTimeInterval The duration of the activity.
* @Groups({"event_activities"})
* @ORM\Column(type="dateinterval", nullable=true)
*/
private $duration;
/**
* @var VenueLoc The venue or location of the activity in accordance to the map for the event.
*
* @ORM\ManyToOne(targetEntity="App\Entity\VenueLoc", inversedBy="activities")
* @Groups({"event_activities"})
*/
private $venueLoc;
/**
* @var Event The event this activity is associated with.
*
* @ORM\ManyToOne(targetEntity="App\Entity\Event", inversedBy="activities")
*/
private $event;
More properties, getters, setters, etc...
并在 service.yaml.
#This is required for the custom Data Provider to use all the ApiPlatform's built in extensions like order, filter, pagination, etc.
#See EventRequireCollectionDataProvider for an example on how to grab and use all the built in extensions
#Allows for the custom Data Provider to act like a normal Data Provider
#Only works for Doctrine ORM I guess.
App\DataProvider\EventRequireCollectionDataProvider:
arguments:
$collectionExtensions: !tagged api_platform.doctrine.orm.query_extension.collection
#This is required to call the custom swagger decorator to correct the parameters documentation to also require the filter and not allow any other parameters
App\Swagger\SwaggerEventRequireDecorator:
decorates: 'api_platform.swagger.normalizer.api_gateway'
arguments: [ '@App\Swagger\SwaggerEventRequireDecorator.inner' ]
autoconfigure: false
并在 App\Swagger 中修复网关的 Swagger 文档。
<?php
namespace App\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SwaggerEventRequireDecorator implements NormalizerInterface
{
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
$this->decorated = $decorated;
}
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decorated->normalize($object, $format, $context);
$customDefinition = [
'name' => 'event',
'description' => 'ID of the event the activities belong to.',
'in' => 'query',
'required' => 'true',
'type' => 'integer'
];
// e.g. remove an existing event parameter
$docs['paths']['/scheduleamap-api/activities']['get']['parameters'] = array_values(array_filter($docs['paths']['/scheduleamap-api/activities']['get']['parameters'], function ($param) {
return $param['name'] !== 'event';
}));
// e.g. add the new definition for event
$docs['paths']['/scheduleamap-api/activities']['get']['parameters'][] = $customDefinition;
// Remove other restricted parameters that will generate errors.
$docs['paths']['/scheduleamap-api/activities']['get']['parameters'] = array_values(array_filter($docs['paths']['/scheduleamap-api/activities']['get']['parameters'], function ($param) {
return $param['name'] !== 'event[]';
}));
return $docs;
}
public function supportsNormalization($data, $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
}
我使用 API 平台并且我在 https://api-platform.com/docs/core/filters/#creating-custom-filters
之后定义了一个自定义过滤器它工作正常,但是我需要每次应用程序对特定实体(设置过滤器的地方)执行 GET HTTP 请求时都需要过滤器。
我检查过这段代码:
// This function is only used to hook in documentation generators (supported by Swagger and Hydra)
public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}
$description = [];
foreach ($this->properties as $property => $strategy) {
$description["similar_$property"] = [
'property' => $property,
'type' => 'string',
'required' => false,
'swagger' => [
'description' => 'Filter using a similar postgres function.',
'name' => $property,
'type' => 'string',
],
];
}
return $description;
}
虽然 getDescription 有必填字段,但它仅适用于 api 文档而不适用于 HTTP 请求
您可以通过事件系统强制执行过滤器。
/**
* @ApiResource
*/
class Resource {
...
}
<?php
namespace App\EventSubscriber;
use ApiPlatform\Core\EventListener\EventPriorities;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
class ResourceReadSubscriber implements EventSubscriberInterface
{
/**
* @return array The event names to listen to
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::CONTROLLER => [
['hasFilter', EventPriorities::PRE_READ],
],
];
}
public function hasFilter(ControllerEvent $event)
{
// first check if this affects the requested resource
$resource = $event->getRequest()->attributes->get('_api_resource_class');
if (Resource::class !== $resource) {
return;
}
// second check if this is the get_collection controller
$controller = $event->getRequest()->attributes->get('_controller');
if ('api_platform.action.get_collection' !== $controller) {
return;
}
// third validate the required filter is set
// we expect a filter via GET parameter 'filter-query-parameter'
if (!$event->getRequest()->query->has('filter-query-parameter')) {
throw new BadRequestHttpException('Filter is required');
}
}
}
我使用自定义数据提供程序实施了过滤器。
在自动连接和自动配置都设置为 true 的情况下,我将其添加到 App\DataProvider:
<?php
namespace App\DataProvider;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\FilterExtension;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface;
use Doctrine\Common\Persistence\ManagerRegistry;
use ApiPlatform\Core\Exception\RuntimeException;
use App\Entity\Activity;
final class EventRequireCollectionDataProvider implements CollectionDataProviderInterface, RestrictedDataProviderInterface
{
private $collectionExtensions;
private $managerRegistry;
public function __construct(ManagerRegistry $managerRegistry, iterable $collectionExtensions)
{
$this->managerRegistry = $managerRegistry;
$this->collectionExtensions = $collectionExtensions;
}
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Activity::class === $resourceClass;
}
/**
* {@inheritdoc}
*
* @throws RuntimeException
*/
public function getCollection(string $resourceClass, string $operationName = null, array $context = [])
{
//This is where the filter is enforced...
if (empty($context) || !array_key_exists ( "filters" , $context ) || !array_key_exists ( "event" , $context['filters'] ) || !ctype_digit($context['filters']['event'])){
throw new RuntimeException('The api call must have the event id parameter set.');
}
$manager = $this->managerRegistry->getManagerForClass($resourceClass);
$repository = $manager->getRepository($resourceClass);
$queryBuilder = $repository->createQueryBuilder('o');
$queryNameGenerator = new QueryNameGenerator();
//This goes through all the built in extensions Api Platform provides.
//The filter is automatically added to the query here
foreach ($this->collectionExtensions as $extension) {
$extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName)){
$result = $extension->getResult($queryBuilder, $resourceClass, $operationName, $context);
return $result;
}
}
$queryBuilder->getQuery()->getResult();
}
}
我的实体(重要的东西 - 我的实体也使用组标准化器):
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
use ApiPlatform\Core\Action\NotFoundAction;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use App\Controller\DoNothingController;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ApiResource(
* iri="http://schema.org/Event",
* collectionOperations={
* "get" = {
* "normalization_context"={"groups"={"event_activities"}},
* "swagger_context"={
* "summary" = "Retrieves the activities for a specific event.",
* "description" = "This operation will obtain all the activities (i.e., the schedule) for a particular event. Currently, the event parameter is required and only one can be submitted at a time. Otherwise, an error will occur."
* },
* }
* },
* itemOperations={
* "get" = {
* "controller"=NotFoundAction::class,
* "read"=false,
* "output"=false,
* }
* },
* )
* @ApiFilter(SearchFilter::class, properties={"event": "exact"})
* @ORM\Entity(repositoryClass="App\Repository\ActivityRepository")
*/
class Activity
{
/**
* @var integer The activity identifier
*
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
* @Groups({"event_activities"})
*/
private $id;
/**
* @var \DateTime The start time for the activity.
* @ORM\Column(type="datetimetz", nullable=true)
* @Groups({"event_activities"})
*/
private $startTime;
/**
* @var \DateTime The ending time for the activity.
* @ORM\Column(type="datetimetz", nullable=true)
* @Groups({"event_activities"})
* @ApiProperty(iri="http://schema.org/endDate")
*/
private $endTime;
/**
* @var \DateTimeInterval The duration of the activity.
* @Groups({"event_activities"})
* @ORM\Column(type="dateinterval", nullable=true)
*/
private $duration;
/**
* @var VenueLoc The venue or location of the activity in accordance to the map for the event.
*
* @ORM\ManyToOne(targetEntity="App\Entity\VenueLoc", inversedBy="activities")
* @Groups({"event_activities"})
*/
private $venueLoc;
/**
* @var Event The event this activity is associated with.
*
* @ORM\ManyToOne(targetEntity="App\Entity\Event", inversedBy="activities")
*/
private $event;
More properties, getters, setters, etc...
并在 service.yaml.
#This is required for the custom Data Provider to use all the ApiPlatform's built in extensions like order, filter, pagination, etc.
#See EventRequireCollectionDataProvider for an example on how to grab and use all the built in extensions
#Allows for the custom Data Provider to act like a normal Data Provider
#Only works for Doctrine ORM I guess.
App\DataProvider\EventRequireCollectionDataProvider:
arguments:
$collectionExtensions: !tagged api_platform.doctrine.orm.query_extension.collection
#This is required to call the custom swagger decorator to correct the parameters documentation to also require the filter and not allow any other parameters
App\Swagger\SwaggerEventRequireDecorator:
decorates: 'api_platform.swagger.normalizer.api_gateway'
arguments: [ '@App\Swagger\SwaggerEventRequireDecorator.inner' ]
autoconfigure: false
并在 App\Swagger 中修复网关的 Swagger 文档。
<?php
namespace App\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SwaggerEventRequireDecorator implements NormalizerInterface
{
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
$this->decorated = $decorated;
}
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decorated->normalize($object, $format, $context);
$customDefinition = [
'name' => 'event',
'description' => 'ID of the event the activities belong to.',
'in' => 'query',
'required' => 'true',
'type' => 'integer'
];
// e.g. remove an existing event parameter
$docs['paths']['/scheduleamap-api/activities']['get']['parameters'] = array_values(array_filter($docs['paths']['/scheduleamap-api/activities']['get']['parameters'], function ($param) {
return $param['name'] !== 'event';
}));
// e.g. add the new definition for event
$docs['paths']['/scheduleamap-api/activities']['get']['parameters'][] = $customDefinition;
// Remove other restricted parameters that will generate errors.
$docs['paths']['/scheduleamap-api/activities']['get']['parameters'] = array_values(array_filter($docs['paths']['/scheduleamap-api/activities']['get']['parameters'], function ($param) {
return $param['name'] !== 'event[]';
}));
return $docs;
}
public function supportsNormalization($data, $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
}