Allow to cache amazon search results to reduce API calls

This commit is contained in:
Jan Böhmer 2026-02-22 22:29:44 +01:00
parent 258289482b
commit 87919eb445
2 changed files with 60 additions and 5 deletions

View file

@ -30,6 +30,7 @@ use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\BuerklinSettings; use App\Settings\InfoProviderSystem\BuerklinSettings;
use App\Settings\InfoProviderSystem\CanopySettings; use App\Settings\InfoProviderSystem\CanopySettings;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\DependencyInjection\Attribute\When; use Symfony\Component\DependencyInjection\Attribute\When;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
@ -45,7 +46,8 @@ class CanopyProvider implements InfoProviderInterface
public const DISTRIBUTOR_NAME = 'Amazon'; public const DISTRIBUTOR_NAME = 'Amazon';
public function __construct(private readonly CanopySettings $settings, private readonly HttpClientInterface $httpClient) public function __construct(private readonly CanopySettings $settings,
private readonly HttpClientInterface $httpClient, private readonly CacheItemPoolInterface $partInfoCache)
{ {
} }
@ -76,6 +78,39 @@ class CanopyProvider implements InfoProviderInterface
return "https://www.amazon.{$this->settings->domain}/dp/{$asin}"; return "https://www.amazon.{$this->settings->domain}/dp/{$asin}";
} }
/**
* Saves the given part to the cache.
* Everytime this function is called, the cache is overwritten.
* @param PartDetailDTO $part
* @return void
*/
private function saveToCache(PartDetailDTO $part): void
{
$key = 'canopy_part_'.$part->provider_id;
$item = $this->partInfoCache->getItem($key);
$item->set($part);
$item->expiresAfter(3600 * 24); //Cache for 1 day
$this->partInfoCache->save($item);
}
/**
* Retrieves a from the cache, or null if it was not cached yet.
* @param string $id
* @return PartDetailDTO|null
*/
private function getFromCache(string $id): ?PartDetailDTO
{
$key = 'canopy_part_'.$id;
$item = $this->partInfoCache->getItem($key);
if ($item->isHit()) {
return $item->get();
}
return null;
}
public function searchByKeyword(string $keyword): array public function searchByKeyword(string $keyword): array
{ {
$response = $this->httpClient->request('GET', self::SEARCH_API_URL, [ $response = $this->httpClient->request('GET', self::SEARCH_API_URL, [
@ -93,14 +128,20 @@ class CanopyProvider implements InfoProviderInterface
$out = []; $out = [];
foreach ($results as $result) { foreach ($results as $result) {
$out[] = new SearchResultDTO(
$dto = new PartDetailDTO(
provider_key: $this->getProviderKey(), provider_key: $this->getProviderKey(),
provider_id: $result['asin'], provider_id: $result['asin'],
name: $result["title"], name: $result["title"],
description: "", description: "",
preview_image_url: $result["mainImageUrl"] ?? null, preview_image_url: $result["mainImageUrl"] ?? null,
provider_url: $this->productPageFromASIN($result['asin']), provider_url: $this->productPageFromASIN($result['asin']),
vendor_infos: [$this->priceToPurchaseInfo($result['price'], $result['asin'])]
); );
$out[] = $dto;
$this->saveToCache($dto);
} }
return $out; return $out;
@ -125,11 +166,15 @@ class CanopyProvider implements InfoProviderInterface
return $notes; return $notes;
} }
private function priceToPurchaseInfo(array $price, string $asin): PurchaseInfoDTO private function priceToPurchaseInfo(?array $price, string $asin): PurchaseInfoDTO
{ {
$priceDto = new PriceDTO(minimum_discount_amount: 1, price: (string) $price['value'], currency_iso_code: $price['currency'], includes_tax: true); $priceDtos = [];
if ($price !== null) {
$priceDtos[] = new PriceDTO(minimum_discount_amount: 1, price: (string) $price['value'], currency_iso_code: $price['currency'], includes_tax: true);
}
return new PurchaseInfoDTO(self::DISTRIBUTOR_NAME, order_number: $asin, prices: [$priceDto], product_url: $this->productPageFromASIN($asin));
return new PurchaseInfoDTO(self::DISTRIBUTOR_NAME, order_number: $asin, prices: $priceDtos, product_url: $this->productPageFromASIN($asin));
} }
public function getDetails(string $id): PartDetailDTO public function getDetails(string $id): PartDetailDTO
@ -139,6 +184,11 @@ class CanopyProvider implements InfoProviderInterface
throw new \InvalidArgumentException("The id must be a valid ASIN (10 characters, letters and numbers)"); throw new \InvalidArgumentException("The id must be a valid ASIN (10 characters, letters and numbers)");
} }
//Use cached details if available and the settings allow it, to avoid unnecessary API requests, since the search results already contain most of the details
if(!$this->settings->alwaysGetDetails && ($cached = $this->getFromCache($id)) !== null) {
return $cached;
}
$response = $this->httpClient->request('GET', self::DETAIL_API_URL, [ $response = $this->httpClient->request('GET', self::DETAIL_API_URL, [
'query' => [ 'query' => [
'asin' => $id, 'asin' => $id,

View file

@ -43,4 +43,9 @@ class CanopySettings
public ?string $apiKey = null; public ?string $apiKey = null;
public string $domain = "de"; public string $domain = "de";
/**
* @var bool If true, the provider will always retrieve details for a part, resulting in an additional API request
*/
public bool $alwaysGetDetails = false;
} }