Filter required in Api Platform

无人久伴 提交于 2019-12-13 03:40:58

问题


Im using API Platform and I have defined a custom filter following https://api-platform.com/docs/core/filters/#creating-custom-filters

It works ok, but I need to that filter is required everytime that application do a GET HTTP Request of particular entity (where the filter is set).

I have check this code:

// 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;
}

Although getDescription has a required field, it only works for api documentation and not for HTTP Request


回答1:


You can enforce a filter through the event system.

/**
 * @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');
        }
    }
}



回答2:


The ApiFilters are designed to be not required.




回答3:


I enforced a filter using a custom data provider.

With both auto wire and autoconfigure set to true, I added this to 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();
        }
    }

My entity (the stuff that matters - my entity also uses a group normalizer):

<?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...

And in 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

And in App\Swagger to fix the Swagger documentation at the gateway.

<?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);
    }
}


来源:https://stackoverflow.com/questions/55020369/filter-required-in-api-platform

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!