diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index eb80d9bc..4c7ec37f 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -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) { diff --git a/src/Services/InfoProviderSystem/PartInfoRetriever.php b/src/Services/InfoProviderSystem/PartInfoRetriever.php index db1895e7..53206a75 100644 --- a/src/Services/InfoProviderSystem/PartInfoRetriever.php +++ b/src/Services/InfoProviderSystem/PartInfoRetriever.php @@ -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 $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 $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); }); } diff --git a/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php b/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php index e5017eb2..ca6e26e1 100644 --- a/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php +++ b/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php @@ -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); } diff --git a/src/Services/InfoProviderSystem/Providers/CanopyProvider.php b/src/Services/InfoProviderSystem/Providers/CanopyProvider.php index d826c765..aee30d6b 100644 --- a/src/Services/InfoProviderSystem/Providers/CanopyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/CanopyProvider.php @@ -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; } diff --git a/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php b/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php index feb3466f..8896d94b 100644 --- a/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php +++ b/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php @@ -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 diff --git a/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php b/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php index 2c98e6e5..9764517b 100644 --- a/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php +++ b/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php @@ -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()) { diff --git a/src/Services/InfoProviderSystem/Providers/OctopartProvider.php b/src/Services/InfoProviderSystem/Providers/OctopartProvider.php index 684e4ff0..de404e18 100644 --- a/src/Services/InfoProviderSystem/Providers/OctopartProvider.php +++ b/src/Services/InfoProviderSystem/Providers/OctopartProvider.php @@ -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; }