mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-01-13 13:49:33 +00:00
Added info provider for Buerklin (#1151)
* Fixed Typos and mistranslations in GDPR mode (DSGVO Modus) Fixed Typo enviroment * Create BuerklinProvider based on LCSCProvider * Update GET URLs for Buerklin * Add getToken function analog to Octopart * Remove line break in docs * Remove trailing / in ENDPOINT_URL Use Autowire to use values of environment variables Remove unwanted Code from LCSC-Provider Map json response to DTO variables * Fix variable reference errors ($term → $keyword) Ensure array keys exist before accessing them Optimize API calls to prevent unnecessary requests Improve error handling for better debugging Enhance readability and maintainability of functions * Bumped version v1.16.2 * Update BuerklinProvider.php Change Order of Capabilities * Change order of capabilities in LCSCProvider.php * Change order of capabilities in PollinProvider.php * Try to fix getToken BuerklinProvider.php * Add ip_buerklin_oauth to knpu_oauth2_client.yaml * Update buerklin authorize URL in knpu_oauth2_client.yaml * Update knpu_oauth2_client.yaml * Adapt Buerklin InfoProvider to new Settings mechanism * According to Buerklin API spec it's really 'token' as urlAuthorize endpoint * Rückgabewert ist schon ein Array deshalb kein toArray * Fix API-Access, add image, price and parameter retrieval (Datasheets not yet implemented because it is not available in the API response) * Add Caching of requests, use default query params (language and currency) using a function, Fix Footprint assignment, translate German code comments * Remove DATASHEET from ProviderCapabilities because the Bürklin API doesn't include Datasheet URLs at the moment, more reverse engineering needed * Update BuerklinSettings with existing translatable strings * Improve Buerklin Settings Page * Added Translation strings for Buerklin Info Provider * Improve Buerklin Provider help message * Adapt Buerklin-provider to new settings system * Adapt Buerklin-provider to new settings system: add missing instance of BuerklinSettings * Improve Compliance Parameters parsing * Remove language-dependent RoHs Date code and use shortened ISO format, Add Customs Code without parseValueField * Fix no results for keyword search * Implement BatchInfoProviderInterface for Buerklin Provider * Rename searchBatch to searchByKeywordsBatch to correctly implement BatchInfoProviderInterface * Fix Bulk Info Provider Import for Buerklin * Tranlate comments to English to prepare for Pull-Request * Add phpUnit unit tests for BuerklinProvider * Try fixing PHPStan issues * Remove OAuthTokenManager from BuerklinProviderTest Removed OAuthTokenManager mock from BuerklinProviderTest setup. * Fix Settings must not be instantiated directly * Fix UnitTest for value_typ *edd5fb3319 (r2622249199)Revert "Change order of capabilities in LCSCProvider.php" This reverts commitdfd6f33e52. *edd5fb3319 (r2622249861)Revert "Change order of capabilities in PollinProvider.php" This reverts commitfc2e7265be. * Use language setting for ProductShortURL * Update default language for Buerklin provider to English in documentation * Add suggested improvements from SonarQube * Removed unused use directives * Revert SonarQube proposed change. Having more than one return is acceptable nowadays * Improve getProviderInfo: disable oauth_app_name, add settings_class, improve disabled_help * Implement retrieveROPCToken as proposed in https://github.com/Part-DB/Part-DB-server/pull/1151#discussion_r2622976206 * Add missing ) to retrieveROPCToken * add use OAuthTokenManager and create instance in constructor * Revert the following commits that tried to implement getToken using OAuthTokenManager Revert "add use OAuthTokenManager and create instance in constructor"This reverts commit 2a1e7c9b0974ebd7e082d5a2fa62753d6254a767.Revert "Add missing ) to retrieveROPCToken"This reverts commit8df5cfc49e. Revert "Implement retrieveROPCToken as proposed in https://github.com/Part-DB/Part-DB-server/pull/1151#discussion_r2622976206" This reverts commit66cc732082. * Remove OAuthTokenManager leftovers * Disable buerklin provider if settings fields are empty * Improved docs * Added TODO comment --------- Co-authored-by: root <root@part-db.fritz.box> Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
This commit is contained in:
parent
7116c2ceb9
commit
be35c36c58
9 changed files with 1170 additions and 114 deletions
639
src/Services/InfoProviderSystem/Providers/BuerklinProvider.php
Normal file
639
src/Services/InfoProviderSystem/Providers/BuerklinProvider.php
Normal file
|
|
@ -0,0 +1,639 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
* Copyright (C) 2025 Marc Kreidler (https://github.com/mkne)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Services\InfoProviderSystem\Providers;
|
||||
|
||||
use App\Services\InfoProviderSystem\DTOs\FileDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use App\Settings\InfoProviderSystem\BuerklinSettings;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class BuerklinProvider implements BatchInfoProviderInterface
|
||||
{
|
||||
|
||||
private const ENDPOINT_URL = 'https://www.buerklin.com/buerklinws/v2/buerklin';
|
||||
|
||||
public const DISTRIBUTOR_NAME = 'Buerklin';
|
||||
|
||||
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(
|
||||
private readonly HttpClientInterface $client,
|
||||
private readonly CacheItemPoolInterface $partInfoCache,
|
||||
private readonly BuerklinSettings $settings,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latest OAuth token for the Buerklin API, or creates a new one if none is available
|
||||
* TODO: Rework this to use the OAuth token manager system in the database...
|
||||
* @return string
|
||||
*/
|
||||
private function getToken(): string
|
||||
{
|
||||
// Cache token to avoid hammering the auth server on every request
|
||||
$cacheKey = 'buerklin.oauth.token';
|
||||
$item = $this->partInfoCache->getItem($cacheKey);
|
||||
|
||||
if ($item->isHit()) {
|
||||
$token = $item->get();
|
||||
if (is_string($token) && $token !== '') {
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
// Buerklin OAuth2 password grant (ROPC)
|
||||
$resp = $this->client->request('POST', 'https://www.buerklin.com/authorizationserver/oauth/token/', [
|
||||
'headers' => [
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||
],
|
||||
'body' => [
|
||||
'grant_type' => 'password',
|
||||
'client_id' => $this->settings->clientId,
|
||||
'client_secret' => $this->settings->secret,
|
||||
'username' => $this->settings->username,
|
||||
'password' => $this->settings->password,
|
||||
],
|
||||
]);
|
||||
|
||||
$data = $resp->toArray(false);
|
||||
|
||||
if (!isset($data['access_token'])) {
|
||||
throw new \RuntimeException(
|
||||
'Invalid token response from Buerklin: HTTP ' . $resp->getStatusCode() . ' body=' . $resp->getContent(false)
|
||||
);
|
||||
}
|
||||
|
||||
$token = (string) $data['access_token'];
|
||||
|
||||
// Cache for (expires_in - 30s) if available
|
||||
$ttl = 300;
|
||||
if (isset($data['expires_in']) && is_numeric($data['expires_in'])) {
|
||||
$ttl = max(60, (int) $data['expires_in'] - 30);
|
||||
}
|
||||
|
||||
$item->set($token);
|
||||
$item->expiresAfter($ttl);
|
||||
$this->partInfoCache->save($item);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
private function getDefaultQueryParams(): array
|
||||
{
|
||||
return [
|
||||
'curr' => $this->settings->currency ?: 'EUR',
|
||||
'language' => $this->settings->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->settings->language . '|' . $this->settings->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
|
||||
{
|
||||
try {
|
||||
$response = $this->client->request('GET', self::ENDPOINT_URL . $endpoint, [
|
||||
'auth_bearer' => $this->getToken(),
|
||||
'headers' => ['Accept' => 'application/json'],
|
||||
'query' => array_merge($this->getDefaultQueryParams(), $queryParams),
|
||||
]);
|
||||
|
||||
return $response->toArray();
|
||||
} catch (\Exception $e) {
|
||||
throw new \RuntimeException("Buerklin API request failed: " .
|
||||
"Endpoint: " . $endpoint .
|
||||
"Token: [redacted] " .
|
||||
"QueryParams: " . json_encode($queryParams, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . " " .
|
||||
"Exception message: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getProviderInfo(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Buerklin',
|
||||
'description' => 'This provider uses the Buerklin API to search for parts.',
|
||||
'url' => 'https://www.buerklin.com/',
|
||||
'disabled_help' => 'Configure the API Client ID, Secret, Username and Password provided by Buerklin in the provider settings to enable.',
|
||||
'settings_class' => BuerklinSettings::class
|
||||
];
|
||||
}
|
||||
|
||||
public function getProviderKey(): string
|
||||
{
|
||||
return 'buerklin';
|
||||
}
|
||||
|
||||
// This provider is considered active if settings are present
|
||||
public function isActive(): bool
|
||||
{
|
||||
// The client credentials and user credentials must be set
|
||||
return $this->settings->clientId !== null && $this->settings->clientId !== ''
|
||||
&& $this->settings->secret !== null && $this->settings->secret !== ''
|
||||
&& $this->settings->username !== null && $this->settings->username !== ''
|
||||
&& $this->settings->password !== null && $this->settings->password !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a field by removing any HTML tags and other unwanted characters
|
||||
* @param string|null $field
|
||||
* @return string|null
|
||||
*/
|
||||
private function sanitizeField(?string $field): ?string
|
||||
{
|
||||
if ($field === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return strip_tags($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a deserialized JSON object of the product and returns a PartDetailDTO
|
||||
* @param array $product
|
||||
* @return PartDetailDTO
|
||||
*/
|
||||
private function getPartDetail(array $product): PartDetailDTO
|
||||
{
|
||||
// If this is a search-result object, it may not contain prices/features/images -> reload full details.
|
||||
if ((!isset($product['price']) && !isset($product['volumePrices'])) && isset($product['code'])) {
|
||||
try {
|
||||
$product = $this->getProduct((string) $product['code']);
|
||||
} catch (\Throwable $e) {
|
||||
// If reload fails, keep the partial product data and continue.
|
||||
}
|
||||
}
|
||||
|
||||
// Extract images from API response
|
||||
$productImages = $this->getProductImages($product['images'] ?? null);
|
||||
|
||||
// Set preview image
|
||||
$preview = $productImages[0]->url ?? null;
|
||||
|
||||
// Extract features (parameters) from classifications[0].features of Buerklin JSON response
|
||||
$features = $product['classifications'][0]['features'] ?? [];
|
||||
|
||||
// Feature parameters (from classifications->features)
|
||||
$featureParams = $this->attributesToParameters($features, ''); // leave group empty for normal parameters
|
||||
|
||||
// Compliance parameters (from top-level fields like RoHS/SVHC/…)
|
||||
$complianceParams = $this->complianceToParameters($product, 'Compliance');
|
||||
|
||||
// Merge all parameters
|
||||
$allParams = array_merge($featureParams, $complianceParams);
|
||||
|
||||
// Assign footprint: "Design" (en) / "Bauform" (de) / "Enclosure" (en) / "Gehäuse" (de)
|
||||
$footprint = null;
|
||||
if (is_array($features)) {
|
||||
foreach ($features as $feature) {
|
||||
$name = $feature['name'] ?? null;
|
||||
if ($name === 'Design' || $name === 'Bauform' || $name === 'Enclosure' || $name === 'Gehäuse') {
|
||||
$footprint = $feature['featureValues'][0]['value'] ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prices: prefer volumePrices, fallback to single price
|
||||
$code = (string) ($product['orderNumber'] ?? $product['code'] ?? '');
|
||||
$prices = $product['volumePrices'] ?? null;
|
||||
|
||||
if (!is_array($prices) || count($prices) === 0) {
|
||||
$pVal = $product['price']['value'] ?? null;
|
||||
$pCur = $product['price']['currencyIso'] ?? ($this->settings->currency ?: 'EUR');
|
||||
|
||||
if (is_numeric($pVal)) {
|
||||
$prices = [
|
||||
[
|
||||
'minQuantity' => 1,
|
||||
'value' => (float) $pVal,
|
||||
'currencyIso' => (string) $pCur,
|
||||
]
|
||||
];
|
||||
} else {
|
||||
$prices = [];
|
||||
}
|
||||
}
|
||||
|
||||
return new PartDetailDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: (string) ($product['code'] ?? $code),
|
||||
|
||||
name: (string) ($product['manufacturerProductId'] ?? $code),
|
||||
description: $this->sanitizeField($product['description'] ?? null),
|
||||
|
||||
category: $this->sanitizeField($product['classifications'][0]['name'] ?? ($product['categories'][0]['name'] ?? null)),
|
||||
manufacturer: $this->sanitizeField($product['manufacturer'] ?? null),
|
||||
mpn: $this->sanitizeField($product['manufacturerProductId'] ?? null),
|
||||
|
||||
preview_image_url: $preview,
|
||||
manufacturing_status: null,
|
||||
|
||||
provider_url: $this->getProductShortURL((string) ($product['code'] ?? $code)),
|
||||
footprint: $footprint,
|
||||
|
||||
datasheets: null, // not found in JSON response, the Buerklin website however has links to datasheets
|
||||
images: $productImages,
|
||||
|
||||
parameters: $allParams,
|
||||
|
||||
vendor_infos: $this->pricesToVendorInfo(
|
||||
sku: $code,
|
||||
url: $this->getProductShortURL($code),
|
||||
prices: $prices
|
||||
),
|
||||
|
||||
mass: $product['weight'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the price array to a VendorInfoDTO array to be used in the PartDetailDTO
|
||||
* @param string $sku
|
||||
* @param string $url
|
||||
* @param array $prices
|
||||
* @return array
|
||||
*/
|
||||
private function pricesToVendorInfo(string $sku, string $url, array $prices): array
|
||||
{
|
||||
$priceDTOs = array_map(function ($price) {
|
||||
$val = $price['value'] ?? null;
|
||||
$valStr = is_numeric($val)
|
||||
? number_format((float) $val, 6, '.', '') // 6 decimal places, trailing zeros are fine
|
||||
: (string) $val;
|
||||
|
||||
// Optional: softly trim unnecessary trailing zeros (e.g. 75.550000 -> 75.55)
|
||||
$valStr = rtrim(rtrim($valStr, '0'), '.');
|
||||
|
||||
return new PriceDTO(
|
||||
minimum_discount_amount: (float) ($price['minQuantity'] ?? 1),
|
||||
price: $valStr,
|
||||
currency_iso_code: (string) ($price['currencyIso'] ?? $this->settings->currency ?? 'EUR'),
|
||||
includes_tax: false
|
||||
);
|
||||
}, $prices);
|
||||
|
||||
return [
|
||||
new PurchaseInfoDTO(
|
||||
distributor_name: self::DISTRIBUTOR_NAME,
|
||||
order_number: $sku,
|
||||
prices: $priceDTOs,
|
||||
product_url: $url,
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a valid Buerklin product short URL from product code
|
||||
* @param string $product_code
|
||||
* @return string
|
||||
*/
|
||||
private function getProductShortURL(string $product_code): string
|
||||
{
|
||||
return 'https://www.buerklin.com/' . $this->settings->language . '/p/' . $product_code . '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a deduplicated list of product images as FileDTOs.
|
||||
*
|
||||
* - takes only real image arrays (with 'url' field)
|
||||
* - makes relative URLs absolute
|
||||
* - deduplicates using URL
|
||||
* - prefers 'zoom' format, then 'product' format, then all others
|
||||
*
|
||||
* @param array|null $images
|
||||
* @return \App\Services\InfoProviderSystem\DTOs\FileDTO[]
|
||||
*/
|
||||
private function getProductImages(?array $images): array
|
||||
{
|
||||
if (!is_array($images)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 1) Only real image entries with URL
|
||||
$imgs = array_values(array_filter($images, fn($i) => is_array($i) && !empty($i['url'])));
|
||||
|
||||
// 2) Prefer zoom images
|
||||
$zoom = array_values(array_filter($imgs, fn($i) => ($i['format'] ?? null) === 'zoom'));
|
||||
$chosen = count($zoom) > 0
|
||||
? $zoom
|
||||
: array_values(array_filter($imgs, fn($i) => ($i['format'] ?? null) === 'product'));
|
||||
|
||||
// 3) If still none, take all
|
||||
if (count($chosen) === 0) {
|
||||
$chosen = $imgs;
|
||||
}
|
||||
|
||||
// 4) Deduplicate by URL (after making absolute)
|
||||
$byUrl = [];
|
||||
foreach ($chosen as $img) {
|
||||
$url = (string) $img['url'];
|
||||
|
||||
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
|
||||
$url = 'https://www.buerklin.com' . $url;
|
||||
}
|
||||
if (!filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$byUrl[$url] = $url;
|
||||
}
|
||||
|
||||
return array_map(
|
||||
fn($url) => new FileDTO($url),
|
||||
array_values($byUrl)
|
||||
);
|
||||
}
|
||||
|
||||
private function attributesToParameters(array $features, ?string $group = null): array
|
||||
{
|
||||
$out = [];
|
||||
|
||||
foreach ($features as $f) {
|
||||
if (!is_array($f)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = $f['name'] ?? null;
|
||||
if (!is_string($name) || trim($name) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$vals = [];
|
||||
foreach (($f['featureValues'] ?? []) as $fv) {
|
||||
if (is_array($fv) && isset($fv['value']) && is_string($fv['value']) && trim($fv['value']) !== '') {
|
||||
$vals[] = trim($fv['value']);
|
||||
}
|
||||
}
|
||||
if (empty($vals)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Multiple values: join with comma
|
||||
$value = implode(', ', array_values(array_unique($vals)));
|
||||
|
||||
// Unit/symbol from Buerklin feature
|
||||
$unit = $f['featureUnit']['symbol'] ?? null;
|
||||
if (!is_string($unit) || trim($unit) === '') {
|
||||
$unit = null;
|
||||
}
|
||||
|
||||
// ParameterDTO parses value field (handles value + unit)
|
||||
$out[] = ParameterDTO::parseValueField(
|
||||
name: $name,
|
||||
value: $value,
|
||||
unit: $unit,
|
||||
symbol: null,
|
||||
group: $group
|
||||
);
|
||||
}
|
||||
|
||||
// Deduplicate by name
|
||||
$byName = [];
|
||||
foreach ($out as $p) {
|
||||
$byName[$p->name] ??= $p;
|
||||
}
|
||||
|
||||
return array_values($byName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PartDetailDTO[]
|
||||
*/
|
||||
public function searchByKeyword(string $keyword): array
|
||||
{
|
||||
$keyword = strtoupper(trim($keyword));
|
||||
if ($keyword === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$response = $this->makeAPICall('/products/search/', [
|
||||
'pageSize' => 50,
|
||||
'currentPage' => 0,
|
||||
'query' => $keyword,
|
||||
'sort' => 'relevance',
|
||||
]);
|
||||
|
||||
$products = $response['products'] ?? [];
|
||||
|
||||
// Normal case: products found in search results
|
||||
if (is_array($products) && !empty($products)) {
|
||||
return array_map(fn($p) => $this->getPartDetail($p), $products);
|
||||
}
|
||||
|
||||
// Fallback: try direct lookup by code
|
||||
try {
|
||||
$product = $this->getProduct($keyword);
|
||||
return [$this->getPartDetail($product)];
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function getDetails(string $id): PartDetailDTO
|
||||
{
|
||||
// Detail endpoint is /products/{code}/
|
||||
$response = $this->getProduct($id);
|
||||
|
||||
return $this->getPartDetail($response);
|
||||
}
|
||||
|
||||
public function getCapabilities(): array
|
||||
{
|
||||
return [
|
||||
ProviderCapabilities::BASIC,
|
||||
ProviderCapabilities::PICTURE,
|
||||
//ProviderCapabilities::DATASHEET, // currently not implemented
|
||||
ProviderCapabilities::PRICE,
|
||||
ProviderCapabilities::FOOTPRINT,
|
||||
];
|
||||
}
|
||||
|
||||
private function complianceToParameters(array $product, ?string $group = 'Compliance'): array
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$add = function (string $name, $value) use (&$params, $group) {
|
||||
if ($value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
$value = $value ? 'Yes' : 'No';
|
||||
} elseif (is_array($value) || is_object($value)) {
|
||||
// Avoid dumping large or complex structures
|
||||
return;
|
||||
} else {
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$params[] = ParameterDTO::parseValueField(
|
||||
name: $name,
|
||||
value: (string) $value,
|
||||
unit: null,
|
||||
symbol: null,
|
||||
group: $group
|
||||
);
|
||||
};
|
||||
|
||||
$add('RoHS conform', $product['labelRoHS'] ?? null); // "yes"/"no"
|
||||
|
||||
$rawRoHsDate = $product['dateRoHS'] ?? null;
|
||||
// Try to parse and reformat date to Y-m-d (do not use language-dependent formats)
|
||||
if (is_string($rawRoHsDate) && $rawRoHsDate !== '') {
|
||||
try {
|
||||
$dt = new \DateTimeImmutable($rawRoHsDate);
|
||||
$formatted = $dt->format('Y-m-d');
|
||||
} catch (\Exception $e) {
|
||||
$formatted = $rawRoHsDate;
|
||||
}
|
||||
// Always use the same parameter name (do not use language-dependent names)
|
||||
$add('RoHS date', $formatted);
|
||||
}
|
||||
$add('SVHC free', $product['SVHC'] ?? null); // bool
|
||||
$add('Hazardous good', $product['hazardousGood'] ?? null); // bool
|
||||
$add('Hazardous materials', $product['hazardousMaterials'] ?? null); // bool
|
||||
|
||||
$add('Country of origin', $product['countryOfOrigin'] ?? null);
|
||||
// Customs tariff code must always be stored as string, otherwise "85411000" may be stored as "8.5411e+7"
|
||||
if (isset($product['articleCustomsCode'])) {
|
||||
// Raw value as string
|
||||
$codeRaw = (string) $product['articleCustomsCode'];
|
||||
|
||||
// Optionally keep only digits (in case of spaces or other characters)
|
||||
$code = preg_replace('/\D/', '', $codeRaw) ?? $codeRaw;
|
||||
$code = trim($code);
|
||||
|
||||
if ($code !== '') {
|
||||
$params[] = new ParameterDTO(
|
||||
name: 'Customs code',
|
||||
value_text: $code,
|
||||
value_typ: null,
|
||||
value_min: null,
|
||||
value_max: null,
|
||||
unit: null,
|
||||
symbol: null,
|
||||
group: $group
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $keywords
|
||||
* @return array<string, SearchResultDTO[]>
|
||||
*/
|
||||
public function searchByKeywordsBatch(array $keywords): array
|
||||
{
|
||||
/** @var array<string, SearchResultDTO[]> $results */
|
||||
$results = [];
|
||||
|
||||
foreach ($keywords as $keyword) {
|
||||
$keyword = strtoupper(trim((string) $keyword));
|
||||
if ($keyword === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reuse existing single search -> returns PartDetailDTO[]
|
||||
/** @var PartDetailDTO[] $partDetails */
|
||||
$partDetails = $this->searchByKeyword($keyword);
|
||||
|
||||
// Convert to SearchResultDTO[]
|
||||
$results[$keyword] = array_map(
|
||||
fn(PartDetailDTO $detail) => $this->convertPartDetailToSearchResult($detail),
|
||||
$partDetails
|
||||
);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a PartDetailDTO into a SearchResultDTO for bulk search.
|
||||
*/
|
||||
private function convertPartDetailToSearchResult(PartDetailDTO $detail): SearchResultDTO
|
||||
{
|
||||
return new SearchResultDTO(
|
||||
provider_key: $detail->provider_key,
|
||||
provider_id: $detail->provider_id,
|
||||
name: $detail->name,
|
||||
description: $detail->description ?? '',
|
||||
category: $detail->category ?? null,
|
||||
manufacturer: $detail->manufacturer ?? null,
|
||||
mpn: $detail->mpn ?? null,
|
||||
preview_image_url: $detail->preview_image_url ?? null,
|
||||
manufacturing_status: $detail->manufacturing_status ?? null,
|
||||
provider_url: $detail->provider_url ?? null,
|
||||
footprint: $detail->footprint ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -248,4 +248,4 @@ class PollinProvider implements InfoProviderInterface
|
|||
ProviderCapabilities::DATASHEET
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/Settings/InfoProviderSystem/BuerklinSettings.php
Normal file
84
src/Settings/InfoProviderSystem/BuerklinSettings.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
* Copyright (C) 2025 Marc Kreidler (https://github.com/mkne)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Settings\InfoProviderSystem;
|
||||
|
||||
use App\Form\Type\APIKeyType;
|
||||
use App\Settings\SettingsIcon;
|
||||
use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
|
||||
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CountryType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\LanguageType;
|
||||
use Symfony\Component\Translation\TranslatableMessage as TM;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[Settings(label: new TM("settings.ips.buerklin"), description: new TM("settings.ips.buerklin.help"))]
|
||||
#[SettingsIcon("fa-plug")]
|
||||
class BuerklinSettings
|
||||
{
|
||||
use SettingsTrait;
|
||||
|
||||
#[SettingsParameter(
|
||||
label: new TM("settings.ips.digikey.client_id"),
|
||||
formType: APIKeyType::class,
|
||||
envVar: "PROVIDER_BUERKLIN_CLIENT_ID", envVarMode: EnvVarMode::OVERWRITE
|
||||
)]
|
||||
public ?string $clientId = null;
|
||||
|
||||
#[SettingsParameter(
|
||||
label: new TM("settings.ips.digikey.secret"),
|
||||
formType: APIKeyType::class,
|
||||
envVar: "PROVIDER_BUERKLIN_SECRET", envVarMode: EnvVarMode::OVERWRITE
|
||||
)]
|
||||
public ?string $secret = null;
|
||||
|
||||
#[SettingsParameter(
|
||||
label: new TM("settings.ips.buerklin.username"),
|
||||
formType: APIKeyType::class,
|
||||
envVar: "PROVIDER_BUERKLIN_USER", envVarMode: EnvVarMode::OVERWRITE
|
||||
)]
|
||||
public ?string $username = null;
|
||||
|
||||
#[SettingsParameter(
|
||||
label: new TM("user.edit.password"),
|
||||
formType: APIKeyType::class,
|
||||
envVar: "PROVIDER_BUERKLIN_PASSWORD", envVarMode: EnvVarMode::OVERWRITE
|
||||
)]
|
||||
public ?string $password = null;
|
||||
|
||||
#[SettingsParameter(label: new TM("settings.ips.tme.currency"), formType: CurrencyType::class,
|
||||
formOptions: ["preferred_choices" => ["EUR"]],
|
||||
envVar: "PROVIDER_BUERKLIN_CURRENCY", envVarMode: EnvVarMode::OVERWRITE)]
|
||||
#[Assert\Currency()]
|
||||
public string $currency = "EUR";
|
||||
|
||||
#[SettingsParameter(label: new TM("settings.ips.tme.language"), formType: LanguageType::class,
|
||||
formOptions: ["preferred_choices" => ["en", "de"]],
|
||||
envVar: "PROVIDER_BUERKLIN_LANGUAGE", envVarMode: EnvVarMode::OVERWRITE)]
|
||||
#[Assert\Language]
|
||||
public string $language = "en";
|
||||
}
|
||||
|
|
@ -63,4 +63,7 @@ class InfoProviderSettings
|
|||
|
||||
#[EmbeddedSettings]
|
||||
public ?PollinSettings $pollin = null;
|
||||
|
||||
#[EmbeddedSettings]
|
||||
public ?BuerklinSettings $buerklin = null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue