mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-01-13 21:59:34 +00:00
Add Caching of requests, use default query params (language and currency) using a function, Fix Footprint assignment, translate German code comments
This commit is contained in:
parent
73ecbb53cf
commit
cc3fbce962
1 changed files with 73 additions and 44 deletions
|
|
@ -42,6 +42,12 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
|
|
||||||
public const DISTRIBUTOR_NAME = 'Buerklin';
|
public const DISTRIBUTOR_NAME = 'Buerklin';
|
||||||
private const OAUTH_APP_NAME = 'ip_buerklin_oauth';
|
private const OAUTH_APP_NAME = 'ip_buerklin_oauth';
|
||||||
|
private const CACHE_TTL = 600;
|
||||||
|
/**
|
||||||
|
* Local in-request cache to avoid hitting the PSR cache repeatedly for the same product.
|
||||||
|
* @var array<string, array>
|
||||||
|
*/
|
||||||
|
private array $productCache = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly HttpClientInterface $client,
|
private readonly HttpClientInterface $client,
|
||||||
|
|
@ -117,6 +123,43 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
|
|
||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
private function getDefaultQueryParams(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'curr' => $this->currency ?: 'EUR',
|
||||||
|
'language' => $this->language ?: 'en',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getProduct(string $code): array
|
||||||
|
{
|
||||||
|
$code = strtoupper(trim($code));
|
||||||
|
if ($code === '') {
|
||||||
|
throw new \InvalidArgumentException('Product code must not be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheKey = sprintf(
|
||||||
|
'buerklin.product.%s',
|
||||||
|
md5($code . '|' . $this->language . '|' . $this->currency)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($this->productCache[$cacheKey])) {
|
||||||
|
return $this->productCache[$cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = $this->partInfoCache->getItem($cacheKey);
|
||||||
|
if ($item->isHit() && is_array($cached = $item->get())) {
|
||||||
|
return $this->productCache[$cacheKey] = $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product = $this->makeAPICall('/products/' . rawurlencode($code) . '/');
|
||||||
|
|
||||||
|
$item->set($product);
|
||||||
|
$item->expiresAfter(self::CACHE_TTL);
|
||||||
|
$this->partInfoCache->save($item);
|
||||||
|
|
||||||
|
return $this->productCache[$cacheKey] = $product;
|
||||||
|
}
|
||||||
|
|
||||||
private function makeAPICall(string $endpoint, array $queryParams = []): array
|
private function makeAPICall(string $endpoint, array $queryParams = []): array
|
||||||
{
|
{
|
||||||
|
|
@ -124,7 +167,7 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
$response = $this->client->request('GET', self::ENDPOINT_URL . $endpoint, [
|
$response = $this->client->request('GET', self::ENDPOINT_URL . $endpoint, [
|
||||||
'auth_bearer' => $this->getToken(),
|
'auth_bearer' => $this->getToken(),
|
||||||
'headers' => ['Accept' => 'application/json'],
|
'headers' => ['Accept' => 'application/json'],
|
||||||
'query' => array_merge(['curr' => $this->currency ?: 'EUR', 'language' => $this->language ?: 'de'], $queryParams),
|
'query' => array_merge($this->getDefaultQueryParams(), $queryParams),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $response->toArray();
|
return $response->toArray();
|
||||||
|
|
@ -170,10 +213,7 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
*/
|
*/
|
||||||
private function queryDetail(string $id): PartDetailDTO
|
private function queryDetail(string $id): PartDetailDTO
|
||||||
{
|
{
|
||||||
$product = $this->makeAPICall('/products/' . rawurlencode($id) . '/', [
|
$product = $this->getProduct($id);
|
||||||
'curr' => $this->currency,
|
|
||||||
'language' => $this->language,
|
|
||||||
]);
|
|
||||||
if ($product === null) {
|
if ($product === null) {
|
||||||
throw new \RuntimeException('Could not find product code: ' . $id);
|
throw new \RuntimeException('Could not find product code: ' . $id);
|
||||||
}
|
}
|
||||||
|
|
@ -205,38 +245,36 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
// If this is a search-result object, it may not contain prices/features/images -> reload full detail.
|
// If this is a search-result object, it may not contain prices/features/images -> reload full detail.
|
||||||
if ((!isset($product['price']) && !isset($product['volumePrices'])) && isset($product['code'])) {
|
if ((!isset($product['price']) && !isset($product['volumePrices'])) && isset($product['code'])) {
|
||||||
try {
|
try {
|
||||||
$product = $this->makeAPICall('/products/' . rawurlencode((string) $product['code']) . '/');
|
$product = $this->getProduct((string) $product['code']);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// If reload fails, keep the partial product data and continue.
|
// If reload fails, keep the partial product data and continue.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Images (already absolute + dedup in getProductImages())
|
// Extract Images from API response
|
||||||
$productImages = $this->getProductImages($product['images'] ?? null);
|
$productImages = $this->getProductImages($product['images'] ?? null);
|
||||||
|
|
||||||
// Preview image: DO NOT prefix ENDPOINT_URL here (images are already absolute)
|
// Set Preview image
|
||||||
$preview = $productImages[0]->url ?? null;
|
$preview = $productImages[0]->url ?? null;
|
||||||
|
|
||||||
// Features live in classifications[0].features in Bürklin JSON
|
// Extract features (parameters) from classifications[0].features of Bürklin JSON response
|
||||||
$features = $product['classifications'][0]['features'] ?? [];
|
$features = $product['classifications'][0]['features'] ?? [];
|
||||||
$group = $product['classifications'][0]['name'] ?? null;
|
|
||||||
|
|
||||||
|
// Feature Parameters (from classifications->features)
|
||||||
|
$featureParams = $this->attributesToParameters($features, ''); //leave group empty for normal parameters
|
||||||
|
|
||||||
// 1) Feature-Parameter (aus classifications->features)
|
// Compliance-Parameter (from Top-Level fields like RoHS/SVHC/…)
|
||||||
$featureParams = $this->attributesToParameters($features, $group);
|
|
||||||
|
|
||||||
// 2) Compliance-Parameter (aus Top-Level Feldern wie RoHS/SVHC/…)
|
|
||||||
$complianceParams = $this->complianceToParameters($product, 'Compliance');
|
$complianceParams = $this->complianceToParameters($product, 'Compliance');
|
||||||
|
|
||||||
// 3) Zusammenführen
|
// Merge all parameters
|
||||||
$allParams = array_merge($featureParams, $complianceParams);
|
$allParams = array_merge($featureParams, $complianceParams);
|
||||||
|
|
||||||
// Footprint: "Design" (en) / "Bauform" (de)
|
// Assign Footprint: "Design" (en) / "Bauform" (de) / "Enclosure" (en) / "Gehäuse" (de)
|
||||||
$footprint = null;
|
$footprint = null;
|
||||||
if (is_array($features)) {
|
if (is_array($features)) {
|
||||||
foreach ($features as $feature) {
|
foreach ($features as $feature) {
|
||||||
$name = $feature['name'] ?? null;
|
$name = $feature['name'] ?? null;
|
||||||
if ($name === 'Design' || $name === 'Bauform') {
|
if ($name === 'Design' || $name === 'Bauform' || $name === 'Enclosure' || $name === 'Gehäuse') {
|
||||||
$footprint = $feature['featureValues'][0]['value'] ?? null;
|
$footprint = $feature['featureValues'][0]['value'] ?? null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +319,7 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
provider_url: $this->getProductShortURL((string) ($product['code'] ?? $code)),
|
provider_url: $this->getProductShortURL((string) ($product['code'] ?? $code)),
|
||||||
footprint: $footprint,
|
footprint: $footprint,
|
||||||
|
|
||||||
datasheets: null, // not in /products/{code}/ JSON; you decided to skip for now
|
datasheets: null, // not found in JSON response, the Buerklin website however has links to datasheets
|
||||||
images: $productImages,
|
images: $productImages,
|
||||||
|
|
||||||
parameters: $allParams,
|
parameters: $allParams,
|
||||||
|
|
@ -346,12 +384,10 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
/**
|
/**
|
||||||
* Returns a deduplicated list of product images as FileDTOs.
|
* Returns a deduplicated list of product images as FileDTOs.
|
||||||
*
|
*
|
||||||
* Bürklin liefert oft mehrere Einträge mit gleicher URL (und verschiedene "format"s).
|
* - takes only real image arrays (with 'url' field)
|
||||||
* Diese Variante:
|
* - makes relative URLs absolut
|
||||||
* - nimmt nur echte URL-Strings
|
* - deduplicates using URL
|
||||||
* - macht relative URLs absolut
|
* - prefers 'zoom' format, then 'product' format, then all others
|
||||||
* - dedupliziert nach URL
|
|
||||||
* - bevorzugt zoom/product vor thumbnail
|
|
||||||
*
|
*
|
||||||
* @param array|null $images
|
* @param array|null $images
|
||||||
* @return \App\Services\InfoProviderSystem\DTOs\FileDTO[]
|
* @return \App\Services\InfoProviderSystem\DTOs\FileDTO[]
|
||||||
|
|
@ -361,21 +397,21 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
if (!is_array($images))
|
if (!is_array($images))
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
// 1) Nur echte Image-Arrays
|
// 1) Only real image entries with URL
|
||||||
$imgs = array_values(array_filter($images, fn($i) => is_array($i) && !empty($i['url'])));
|
$imgs = array_values(array_filter($images, fn($i) => is_array($i) && !empty($i['url'])));
|
||||||
|
|
||||||
// 2) Bevorzuge zoom; wenn vorhanden, nimm ausschließlich zoom
|
// 2) Prefer zoom images
|
||||||
$zoom = array_values(array_filter($imgs, fn($i) => ($i['format'] ?? null) === 'zoom'));
|
$zoom = array_values(array_filter($imgs, fn($i) => ($i['format'] ?? null) === 'zoom'));
|
||||||
$chosen = count($zoom) > 0
|
$chosen = count($zoom) > 0
|
||||||
? $zoom
|
? $zoom
|
||||||
: array_values(array_filter($imgs, fn($i) => ($i['format'] ?? null) === 'product'));
|
: array_values(array_filter($imgs, fn($i) => ($i['format'] ?? null) === 'product'));
|
||||||
|
|
||||||
// 3) Falls auch keine product-Bilder da sind, nimm alles (letzter Fallback)
|
// 3) If still none, take all
|
||||||
if (count($chosen) === 0) {
|
if (count($chosen) === 0) {
|
||||||
$chosen = $imgs;
|
$chosen = $imgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Dedupliziere nach URL + relativ -> absolut
|
// 4) Deduplicate by URL (after making absolute)
|
||||||
$byUrl = [];
|
$byUrl = [];
|
||||||
foreach ($chosen as $img) {
|
foreach ($chosen as $img) {
|
||||||
$url = (string) $img['url'];
|
$url = (string) $img['url'];
|
||||||
|
|
@ -417,7 +453,6 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
if (!is_string($name) || trim($name) === '')
|
if (!is_string($name) || trim($name) === '')
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Bürklin: featureValues ist ein Array von { value: "..." }
|
|
||||||
$vals = [];
|
$vals = [];
|
||||||
foreach (($f['featureValues'] ?? []) as $fv) {
|
foreach (($f['featureValues'] ?? []) as $fv) {
|
||||||
if (is_array($fv) && isset($fv['value']) && is_string($fv['value']) && trim($fv['value']) !== '') {
|
if (is_array($fv) && isset($fv['value']) && is_string($fv['value']) && trim($fv['value']) !== '') {
|
||||||
|
|
@ -427,16 +462,16 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
if (count($vals) === 0)
|
if (count($vals) === 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Mehrfachwerte zusammenführen
|
// Multiple values: join with comma
|
||||||
$value = implode(', ', array_values(array_unique($vals)));
|
$value = implode(', ', array_values(array_unique($vals)));
|
||||||
|
|
||||||
// Unit/Symbol aus Bürklin (optional)
|
// Unit/Symbol from Buerklin feature
|
||||||
$unit = $f['featureUnit']['symbol'] ?? null;
|
$unit = $f['featureUnit']['symbol'] ?? null;
|
||||||
if (!is_string($unit) || trim($unit) === '') {
|
if (!is_string($unit) || trim($unit) === '') {
|
||||||
$unit = null;
|
$unit = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParameterDTO kann Zahl/Range/Unit parsing selbst
|
// ParameterDTO parses value field (handles value+unit)
|
||||||
$out[] = ParameterDTO::parseValueField(
|
$out[] = ParameterDTO::parseValueField(
|
||||||
name: $name,
|
name: $name,
|
||||||
value: $value,
|
value: $value,
|
||||||
|
|
@ -446,7 +481,7 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dedupe nach Name (falls Bürklin doppelt liefert)
|
// deduplicate by name
|
||||||
$byName = [];
|
$byName = [];
|
||||||
foreach ($out as $p) {
|
foreach ($out as $p) {
|
||||||
$byName[$p->name] ??= $p;
|
$byName[$p->name] ??= $p;
|
||||||
|
|
@ -472,15 +507,14 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
|
|
||||||
$products = $response['products'] ?? [];
|
$products = $response['products'] ?? [];
|
||||||
|
|
||||||
// Normalfall: Search liefert Treffer
|
// Normal case: products found in search results
|
||||||
if (is_array($products) && count($products) > 0) {
|
if (is_array($products) && count($products) > 0) {
|
||||||
return array_map(fn($p) => $this->getPartDetail($p), $products);
|
return array_map(fn($p) => $this->getPartDetail($p), $products);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Bestellnummer/Code direkt abfragen
|
// Fallback: try direct lookup by code
|
||||||
// (funktioniert bei deinen Postman-Tests für /products/{code}/)
|
|
||||||
try {
|
try {
|
||||||
$product = $this->makeAPICall('/products/' . rawurlencode($keyword) . '/');
|
$product = $this->getProduct($keyword);
|
||||||
return [$this->getPartDetail($product)];
|
return [$this->getPartDetail($product)];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -492,10 +526,7 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
public function getDetails(string $id): PartDetailDTO
|
public function getDetails(string $id): PartDetailDTO
|
||||||
{
|
{
|
||||||
// Detail endpoint is /products/{code}/
|
// Detail endpoint is /products/{code}/
|
||||||
$response = $this->makeAPICall('/products/' . rawurlencode($id) . '/', [
|
$response = $this->getProduct($id);
|
||||||
'curr' => $this->currency,
|
|
||||||
'language' => $this->language,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->getPartDetail($response);
|
return $this->getPartDetail($response);
|
||||||
}
|
}
|
||||||
|
|
@ -538,13 +569,11 @@ class BuerklinProvider implements InfoProviderInterface
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
$add('RoHS', $product['labelRoHS'] ?? null); // "Ja"
|
$add('RoHS', $product['labelRoHS'] ?? null); // "yes"/"no"
|
||||||
$add('RoHS date', $product['dateRoHS'] ?? null); // ISO string
|
$add('RoHS date', $product['dateRoHS'] ?? null); // ISO string
|
||||||
$add('SVHC', $product['SVHC'] ?? null); // bool
|
$add('SVHC', $product['SVHC'] ?? null); // bool
|
||||||
$add('Hazardous good', $product['hazardousGood'] ?? null); // bool
|
$add('Hazardous good', $product['hazardousGood'] ?? null); // bool
|
||||||
$add('Hazardous materials', $product['hazardousMaterials'] ?? null); // bool
|
$add('Hazardous materials', $product['hazardousMaterials'] ?? null); // bool
|
||||||
|
|
||||||
// Optional, oft nützlich:
|
|
||||||
$add('Country of origin', $product['countryOfOrigin'] ?? null);
|
$add('Country of origin', $product['countryOfOrigin'] ?? null);
|
||||||
$add('Customs code', $product['articleCustomsCode'] ?? null);
|
$add('Customs code', $product['articleCustomsCode'] ?? null);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue