Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

API-Platform: Filtering Custom Data Provider

I'm currently experiencing issues when trying to filter my results when using an external API source (Stripe) in API-Platform.

What I need to be able to do, is return a list of subscriptions for a specified customer. So going to http://localhost/api/subscriptions?customer=123foo would return all records matched to this customer.

Now, the code below is throwing an error because of the ORM\Filter and would be functional without it, as the actual filtering is performed on Stripes API, not by me, BUT, I really want the Swagger-API GUI to have the filter box.

In short, how do I get the Annotations in my Entity to display, searchable fields within the Swagger UI when using an external data source.

What I have is an Entity as below (simplified for example purposes):

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use Symfony\Component\Serializer\Annotation\Groups;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Annotation\ApiFilter;

/**
 * Subscriptions allow you to charge a customer on a recurring basis. A subscription ties a customer to a particular plan you've created.
 * @ApiResource()
 * @ApiFilter(SearchFilter::class, properties={"customer": "exact"})
 * @package App\Entity
 */
class Subscription
{
     /**
     * Unique identifier for the object.
     * @ApiProperty(identifier=true)
     * @var string | null
     */
    protected $id; 

    /**
     * ID of the customer who owns the subscription.
     * @var string | null
     */
    protected $customer;

    // Plus a bunch more properties and their Getters & Setters
}

And the SubscriptionCollectionDataProvider:

<?php

namespace App\DataProvider;

use App\Entity\Subscription;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use App\Controller\BaseController;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Class SubscriptionCollectionDataProvider
 * @package App\DataProvider
 * @author dhayward
 */
final class SubscriptionCollectionDataProvider extends BaseController implements CollectionDataProviderInterface, RestrictedDataProviderInterface
{

    protected $requestStack;

    /**
     * SubscriptionCollectionDataProvider constructor.
     * @param RequestStack $requestStack
     */
    public function __construct(RequestStack $requestStack)
    {
        $this->request = $requestStack->getCurrentRequest();
    }

    /**
     * @param string $resourceClass
     * @param string|null $operationName
     * @param array $context
     * @return bool
     */
    public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
    {
        return Subscription::class === $resourceClass;
    }

    /**
     * @param string $resourceClass
     * @param string|null $operationName
     * @return \Generator
     * @throws \Stripe\Error\Api
     */
    public function getCollection(string $resourceClass, string $operationName = null): \Generator
    {
        $customer = $this->request->get("customer");

        $data = \Stripe\Subscription::all(["customer" => $customer]);

        foreach($data['data'] as $subscriptionObject){
            $this->serializer()->deserialize(json_encode($subscriptionObject), Subscription::class, 'json', array('object_to_populate' => $subscription = new Subscription()));
            yield $subscription;
        }
    }

}

Error result, which is presumably because I'm using an ORM/Filter without any ORM setup:

Call to a member function getClassMetadata() on null

Any pointers would be greatly appreciated.

like image 885
Doug Avatar asked Sep 25 '18 10:09

Doug


1 Answers

So I finally managed to work it out. It was as simple as creating my own version of the SearchFilter, implementing ApiPlatform\Core\Api\FilterInterface.

<?php

namespace App\Filter;

use ApiPlatform\Core\Api\FilterInterface;

/**
 * Class SearchFilter
 * @package App\Filter
 */
class SearchFilter implements FilterInterface
{
    /**
     * @var string Exact matching
     */
    const STRATEGY_EXACT = 'exact';

    /**
     * @var string The value must be contained in the field
     */
    const STRATEGY_PARTIAL = 'partial';

    /**
     * @var string Finds fields that are starting with the value
     */
    const STRATEGY_START = 'start';

    /**
     * @var string Finds fields that are ending with the value
     */
    const STRATEGY_END = 'end';

    /**
     * @var string Finds fields that are starting with the word
     */
    const STRATEGY_WORD_START = 'word_start';

    protected $properties;

    /**
     * SearchFilter constructor.
     * @param array|null $properties
     */
    public function __construct(array $properties = null)
    {
        $this->properties = $properties;
    }

    /**
     * {@inheritdoc}
     */
    public function getDescription(string $resourceClass): array
    {
        $description = [];

        $properties = $this->properties;

        foreach ($properties as $property => $strategy) {

                $filterParameterNames = [
                    $property,
                    $property.'[]',
                ];

                foreach ($filterParameterNames as $filterParameterName) {
                    $description[$filterParameterName] = [
                        'property' => $property,
                        'type' => 'string',
                        'required' => false,
                        'strategy' => self::STRATEGY_EXACT,
                        'is_collection' => '[]' === substr($filterParameterName, -2),
                    ];
                }
            }

        return $description;
    }

}
like image 185
Doug Avatar answered Nov 15 '22 18:11

Doug