Allow to pass options to circumvent caching of info provider results / force fresh
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run

This commit is contained in:
Jan Böhmer 2026-05-01 20:57:41 +02:00
parent f13413a104
commit 4137bde194
7 changed files with 64 additions and 13 deletions

View file

@ -40,6 +40,7 @@ use App\Services\Attachments\AttachmentSubmitHandler;
use App\Services\Attachments\PartPreviewGenerator;
use App\Services\EntityMergers\Mergers\PartMerger;
use App\Services\InfoProviderSystem\PartInfoRetriever;
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
use App\Services\LogSystem\EventCommentHelper;
use App\Services\LogSystem\HistoryHelper;
use App\Services\LogSystem\TimeTravel;
@ -283,7 +284,10 @@ final class PartController extends AbstractController
{
$this->denyAccessUnlessGranted('@info_providers.create_parts');
$dto = $infoRetriever->getDetails($providerKey, $providerId);
//Force info providers to not use cache, when retrieving part details for creating a new part, because otherwise we might end up with outdated information
$no_cache = $request->query->getBoolean('no_cache', false);
$dto = $infoRetriever->getDetails($providerKey, $providerId, [InfoProviderInterface::OPTION_NO_CACHE => $no_cache]);
$new_part = $infoRetriever->dtoToPart($dto);
if ($new_part->getCategory() === null || $new_part->getCategory()->getID() === null) {

View file

@ -53,6 +53,7 @@ final class PartInfoRetriever
* Search for a keyword in the given providers. The results can be cached
* @param string[]|InfoProviderInterface[] $providers A list of providers to search in, either as provider keys or as provider instances
* @param string $keyword The keyword to search for
* @param array<string, mixed> $options An associative array of options which can be used to modify the search behavior. The supported options depend on the provider and should be documented in the provider's documentation.
* @return SearchResultDTO[] The search results
* @throws InfoProviderNotActiveException if any of the given providers is not active
* @throws ClientException if any of the providers throws an exception during the search
@ -60,7 +61,7 @@ final class PartInfoRetriever
* @throws TransportException if any of the providers throws an exception during the search
* @throws OAuthReconnectRequiredException if any of the providers throws an exception during the search that indicates that the OAuth token needs to be refreshed
*/
public function searchByKeyword(string $keyword, array $providers): array
public function searchByKeyword(string $keyword, array $providers, array $options = []): array
{
$results = [];
@ -89,15 +90,31 @@ final class PartInfoRetriever
* Search for a keyword in the given provider. The result is cached for 7 days.
* @return SearchResultDTO[]
*/
protected function searchInProvider(InfoProviderInterface $provider, string $keyword): array
protected function searchInProvider(InfoProviderInterface $provider, string $keyword, array $options = []): array
{
//Generate key and escape reserved characters from the provider id
$escaped_keyword = hash('xxh3', $keyword);
return $this->partInfoCache->get("search_{$provider->getProviderKey()}_{$escaped_keyword}", function (ItemInterface $item) use ($provider, $keyword) {
$no_cache = $options[InfoProviderInterface::OPTION_NO_CACHE] ?? false;
//Exclude the no_cache option from the options hash, since it should not affect the cache key, as it only determines whether to bypass the cache or not, but does not change the actual search results
$options_without_cache = $options;
unset($options_without_cache[InfoProviderInterface::OPTION_NO_CACHE]);
//Generate a hash for the options, to ensure that different options result in different cache entries
$options_hash = hash('xxh3', json_encode($options_without_cache, JSON_THROW_ON_ERROR));
$cache_key = "search_{$provider->getProviderKey()}_{$escaped_keyword}_{$options_hash}";
//If no_cache is set, bypass the cache and get fresh results from the provider
if ($no_cache) {
$this->partInfoCache->delete($cache_key);
}
return $this->partInfoCache->get($cache_key, function (ItemInterface $item) use ($provider, $keyword, $options) {
//Set the expiration time
$item->expiresAfter(!$this->debugMode ? self::CACHE_RESULT_EXPIRATION : 10);
return $provider->searchByKeyword($keyword);
return $provider->searchByKeyword($keyword, $options);
});
}
@ -106,10 +123,11 @@ final class PartInfoRetriever
* The result is cached for 4 days.
* @param string $provider_key
* @param string $part_id
* @param array<string, mixed> $options An associative array of options which can be used to modify the search behavior. The supported options depend on the provider and should be documented in the provider's documentation.
* @return PartDetailDTO
* @throws InfoProviderNotActiveException if the the given providers is not active
*/
public function getDetails(string $provider_key, string $part_id): PartDetailDTO
public function getDetails(string $provider_key, string $part_id, array $options = []): PartDetailDTO
{
$provider = $this->provider_registry->getProviderByKey($provider_key);
@ -118,13 +136,26 @@ final class PartInfoRetriever
throw InfoProviderNotActiveException::fromProvider($provider);
}
//Exclude the no_cache option from the options hash, since it should not affect the cache key, as it only determines whether to bypass the cache or not, but does not change the actual search results
$options_without_cache = $options;
unset($options_without_cache[InfoProviderInterface::OPTION_NO_CACHE]);
//Generate a hash for the options, to ensure that different options result in different cache entries
$options_hash = hash('xxh3', json_encode($options_without_cache, JSON_THROW_ON_ERROR));
//Generate key and escape reserved characters from the provider id
$escaped_part_id = hash('xxh3', $part_id);
return $this->partInfoCache->get("details_{$provider_key}_{$escaped_part_id}", function (ItemInterface $item) use ($provider, $part_id) {
$cache_key = "details_{$provider_key}_{$escaped_part_id}_{$options_hash}";
//Delete the cache entry if no_cache is set, to ensure that the next get call will fetch fresh data from the provider, instead of returning stale data from the cache.
if ($options[InfoProviderInterface::OPTION_NO_CACHE] ?? false) {
$this->partInfoCache->delete($cache_key);
}
return $this->partInfoCache->get($cache_key, function (ItemInterface $item) use ($provider, $part_id, $options) {
//Set the expiration time
$item->expiresAfter(!$this->debugMode ? self::CACHE_DETAIL_EXPIRATION : 10);
return $provider->getDetails($part_id);
return $provider->getDetails($part_id, $options);
});
}

View file

@ -120,7 +120,7 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv
];
}
private function getProduct(string $code): array
private function getProduct(string $code, bool $use_cache = true): array
{
$code = strtoupper(trim($code));
if ($code === '') {
@ -132,6 +132,11 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv
md5($code . '|' . $this->settings->language . '|' . $this->settings->currency)
);
if (!$use_cache) {
$this->partInfoCache->deleteItem($cacheKey);
unset($this->productCache[$cacheKey]);
}
if (isset($this->productCache[$cacheKey])) {
return $this->productCache[$cacheKey];
}
@ -488,7 +493,7 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv
// Fallback: try direct lookup by code
try {
$product = $this->getProduct($keyword);
$product = $this->getProduct($keyword, use_cache: !($options[self::OPTION_NO_CACHE] ?? false));
return [$this->getPartDetail($product)];
} catch (\Throwable $e) {
return [];
@ -498,7 +503,8 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv
public function getDetails(string $id, array $options = []): PartDetailDTO
{
// Detail endpoint is /products/{code}/
$response = $this->getProduct($id);
//By default use cache for details, but allow bypassing cache with option (e.g. for refresh)
$response = $this->getProduct($id, use_cache: !($options[self::OPTION_NO_CACHE] ?? false));
return $this->getPartDetail($response);
}

View file

@ -184,8 +184,10 @@ class CanopyProvider implements InfoProviderInterface
throw new \InvalidArgumentException("The id must be a valid ASIN (10 characters, letters and numbers)");
}
$do_not_cache = ($options[self::OPTION_NO_CACHE] ?? false) || $this->settings->alwaysGetDetails;
//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) {
if(!$do_not_cache && ($cached = $this->getFromCache($id)) !== null) {
return $cached;
}

View file

@ -28,6 +28,7 @@ use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
interface InfoProviderInterface
{
public const OPTION_NO_CACHE = 'no_cache'; // if set to true, the provider should not use any cache and retrieve fresh data from the source
/**
* Get information about this provider

View file

@ -424,6 +424,11 @@ class OEMSecretsProvider implements InfoProviderInterface
public function getDetails(string $id, array $options = []): PartDetailDTO
{
$cacheKey = $this->getCacheKey($id);
if ($options[self::OPTION_NO_CACHE] ?? false) {
$this->partInfoCache->deleteItem($cacheKey);
}
$cacheItem = $this->partInfoCache->getItem($cacheKey);
if ($cacheItem->isHit()) {

View file

@ -369,9 +369,11 @@ class OctopartProvider implements InfoProviderInterface
public function getDetails(string $id, array $options = []): PartDetailDTO
{
$no_cache = $options[self::OPTION_NO_CACHE] ?? false;
//Check if we have the part cached
$cached = $this->getFromCache($id);
if ($cached !== null) {
if (!$no_cache && $cached !== null) {
return $cached;
}