saloonphp / saloon

🤠 Build beautiful API integrations and SDKs with Saloon
https://docs.saloon.dev
MIT License
2.09k stars 107 forks source link

Caching with Offset Paginator returns same results for each API call. #330

Closed drfraker closed 11 months ago

drfraker commented 1 year ago

Hey @Sammyjo20, thanks for the amazing package.

I'm running into an issue where I have a paginated connector and a request that is being cached. When I make the call to get paginated results each loop is returning the cached value from the first request and not getting the following pages. Is there something I'm doing wrong here, or is this a bug? It appears to be caching using a key based on the url but not the query parameters and when the params have ?limit=x&offset=y they are not included to cache the paged results.

Here are the connector and the Request:

<?php

namespace App\Http\Integrations\Mindbody;

use App\Models\Property;
use Saloon\Contracts\Authenticator;
use Saloon\Http\Auth\TokenAuthenticator;
use Saloon\Http\Connector;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\PaginationPlugin\Contracts\HasPagination;
use Saloon\PaginationPlugin\OffsetPaginator;
use Saloon\Traits\Plugins\AcceptsJson;

class MindbodyConnector extends Connector implements HasPagination
{
    use AcceptsJson;

    protected ?Property $property;

    public function __construct(Property $property)
    {
        $this->property = $property
            ->loadMissing(['locations:id,property_id,api_id', 'accessToken']);

        return $this;
    }

    /**
     * The Base URL of the API
     */
    public function resolveBaseUrl(): string
    {
        return 'https://api.mindbodyonline.com/public/v6/';
    }

    /**
     * Default headers for every request
     */
    protected function defaultHeaders(): array
    {
        return [
            'Api-Key' => config('platform.mindbody.api_key'),
            'SiteId' => $this->property->api_identifier,
        ];
    }

    public function defaultAuth(): ?Authenticator
    {
        return new TokenAuthenticator($this->property->accessToken->token);
    }

    /**
     * Default HTTP client options
     */
    protected function defaultConfig(): array
    {
        return [
            'timeout' => 30,
        ];
    }

    public function paginate(Request $request): OffsetPaginator
    {
        return new class(connector: $this, request: $request) extends OffsetPaginator
        {
            /**
             * Mindbody Pagination:
             *   "PaginationResponse" => array:4 [
             *     "RequestedLimit" => 100
             *     "RequestedOffset" => 0
             *     "PageSize" => 5
             *     "TotalResults" => 5
             *   ]
             * */
            protected ?int $perPageLimit = 100;

            protected function isLastPage(Response $response): bool
            {
                $is = $this->getOffset() >= (int) $response->json('PaginationResponse.TotalResults');

                return $is;
            }

            protected function getPageItems(Response $response, Request $request): array
            {
                return $response->json($request->itemsKey);
            }
        };
    }
}
<?php

namespace App\Http\Integrations\Mindbody\Requests;

use Saloon\CachePlugin\Contracts\Cacheable;
use Saloon\CachePlugin\Contracts\Driver;
use Saloon\CachePlugin\Drivers\LaravelCacheDriver;
use Saloon\CachePlugin\Traits\HasCaching;
use Saloon\Enums\Method;
use Saloon\Http\PendingRequest;
use Saloon\Http\Request;
use Saloon\PaginationPlugin\Contracts\Paginatable;

class GetProgramIdsRequest extends Request implements Cacheable, Paginatable
{
    use HasCaching;

    /**
     * The HTTP method of the request
     */
    protected Method $method = Method::GET;

    /**
     * The key to retrieve items from the paginated response.
     */
    public string $itemsKey = 'Programs';

    /**
     * The endpoint for the request
     */
    public function resolveEndpoint(): string
    {
        return '/site/programs';
    }

    protected function defaultQuery(): array
    {
        return [
            'scheduleType' => 'All',
        ];
    }

    protected function cacheKey(PendingRequest $pendingRequest): ?string
    {
        return 'tenant_'.tenant()->id.'_mindbody_programs';
    }

    public function resolveCacheDriver(): Driver
    {
        return new LaravelCacheDriver(app('cache.store'));
    }

    public function cacheExpiryInSeconds(): int
    {
        return 600; // 10 minutes

    }
}
drfraker commented 1 year ago

This hack seems to resolve the issue in my case. I'm basically manually adding in the query params to the cache key. Now I get a cached version of each page.

protected function cacheKey(PendingRequest $pendingRequest): ?string
    {
        $query = $pendingRequest->getUri()->getQuery();

        return 'tenant_'.tenant()->id.'_mindbody_programs_'.$query;
    }
drfraker commented 1 year ago

I resolved this issue. I see now that the cache trait calls the CacheKeyHelper and makes a key out of the full request including className, URL, query params and headers. No need to manually define a cache key.

Sammyjo20 commented 11 months ago

Glad you managed to solve the issue! Thank you for the kind words ❤️