$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
+ * @throws \InvalidArgumentException if the given provider key does not match any registered provider
*/
public function getDetails(string $provider_key, string $part_id, array $options = []): PartDetailDTO
{
diff --git a/src/Services/InfoProviderSystem/ProviderRegistry.php b/src/Services/InfoProviderSystem/ProviderRegistry.php
index 18b8a37a..1b0b9785 100644
--- a/src/Services/InfoProviderSystem/ProviderRegistry.php
+++ b/src/Services/InfoProviderSystem/ProviderRegistry.php
@@ -72,7 +72,7 @@ final class ProviderRegistry
private function initStructures(): void
{
foreach ($this->providers as $provider) {
- $key = $provider->getProviderKey();
+ $key = $provider->getProviderInfo()->key;
if (isset($this->providers_by_name[$key])) {
throw new \LogicException("Provider with key $key already registered");
diff --git a/src/Services/InfoProviderSystem/Providers/AIWebProvider.php b/src/Services/InfoProviderSystem/Providers/AIWebProvider.php
index be91041e..feb94384 100644
--- a/src/Services/InfoProviderSystem/Providers/AIWebProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/AIWebProvider.php
@@ -31,6 +31,7 @@ use App\Services\InfoProviderSystem\SubmittedPageStorage;
use App\Services\InfoProviderSystem\CreateFromUrlHelper;
use App\Services\InfoProviderSystem\DTOJsonSchemaConverter;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
+use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Settings\InfoProviderSystem\AIExtractorSettings;
use Jkphl\Micrometa;
use League\HTMLToMarkdown\HtmlConverter;
@@ -50,6 +51,8 @@ final class AIWebProvider implements InfoProviderInterface
{
use FixAndValidateUrlTrait;
+ public const PROVIDER_KEY = 'ai_web';
+
private const DISTRIBUTOR_NAME = 'Website';
private readonly HttpClientInterface $httpClient;
@@ -71,20 +74,23 @@ final class AIWebProvider implements InfoProviderInterface
);
}
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'AI Web Extractor',
- 'description' => 'Extract part info from any URL using LLM',
- //'url' => 'https://openrouter.ai',
- 'disabled_help' => 'Configure AI settings',
- 'settings_class' => AIExtractorSettings::class,
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'ai_web';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'AI Web Extractor',
+ description: 'Extract part info from any URL using LLM',
+ disabledHelp: 'Configure AI settings',
+ settingsClass: AIExtractorSettings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::DATASHEET,
+ ProviderCapabilities::PRICE,
+ ProviderCapabilities::PARAMETERS,
+ ],
+ expensive: true,
+ );
}
public function isActive(): bool
@@ -166,7 +172,7 @@ final class AIWebProvider implements InfoProviderInterface
$llmResponse = $this->callLLM($markdown, $url, $structuredData);
// Build and return PartDetailDTO
- $result = $this->jsonSchemaConverter->jsonToDTO($llmResponse, $this->getProviderKey(), $url, $url, self::DISTRIBUTOR_NAME);
+ $result = $this->jsonSchemaConverter->jsonToDTO($llmResponse, self::PROVIDER_KEY, $url, $url, self::DISTRIBUTOR_NAME);
// Cache the result for future use, to improve performance and reduce costs.
$cacheItem->set($result);
@@ -256,17 +262,6 @@ final class AIWebProvider implements InfoProviderInterface
return $converter->convert($htmlToConvert);
}
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::DATASHEET,
- ProviderCapabilities::PRICE,
- ProviderCapabilities::PARAMETERS,
- ];
- }
-
private function callLLM(string $htmlContent, string $url, ?string $structuredData = null): array
{
$input = new MessageBag(
diff --git a/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php b/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php
index ca6e26e1..9197df48 100644
--- a/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php
@@ -28,6 +28,7 @@ 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\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\BuerklinSettings;
@@ -40,6 +41,7 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv
private const ENDPOINT_URL = 'https://www.buerklin.com/buerklinws/v2/buerklin';
public const DISTRIBUTOR_NAME = 'Buerklin';
+ public const PROVIDER_KEY = 'buerklin';
private const CACHE_TTL = 600;
/**
@@ -175,20 +177,24 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv
}
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- 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';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'Buerklin',
+ description: 'This provider uses the Buerklin API to search for parts.',
+ url: 'https://www.buerklin.com/',
+ disabledHelp: 'Configure the API Client ID, Secret, Username and Password provided by Buerklin in the provider settings to enable.',
+ settingsClass: BuerklinSettings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::PICTURE,
+ //ProviderCapabilities::DATASHEET, // currently not implemented
+ ProviderCapabilities::PRICE,
+ ProviderCapabilities::FOOTPRINT,
+ ProviderCapabilities::PARAMETERS
+ ],
+ );
}
// This provider is considered active if settings are present
@@ -283,7 +289,7 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv
}
return new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: (string) ($product['code'] ?? $code),
name: (string) ($product['manufacturerProductId'] ?? $code),
@@ -509,17 +515,6 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv
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 = [];
diff --git a/src/Services/InfoProviderSystem/Providers/CanopyProvider.php b/src/Services/InfoProviderSystem/Providers/CanopyProvider.php
index aee30d6b..3390bd98 100644
--- a/src/Services/InfoProviderSystem/Providers/CanopyProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/CanopyProvider.php
@@ -26,6 +26,7 @@ namespace App\Services\InfoProviderSystem\Providers;
use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
+use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\BuerklinSettings;
@@ -39,6 +40,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*/
class CanopyProvider implements InfoProviderInterface
{
+ public const PROVIDER_KEY = 'canopy';
public const BASE_URL = "https://rest.canopyapi.co/api";
public const SEARCH_API_URL = self::BASE_URL . "/amazon/search";
@@ -52,20 +54,22 @@ class CanopyProvider implements InfoProviderInterface
}
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'Amazon (Canopy)',
- 'description' => 'Retrieves part infos from Amazon using the Canopy API',
- 'url' => 'https://canopyapi.co',
- 'disabled_help' => 'Set Canopy API key in the provider configuration to enable this provider',
- 'settings_class' => CanopySettings::class
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'canopy';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'Amazon (Canopy)',
+ description: 'Retrieves part infos from Amazon using the Canopy API',
+ url: 'https://canopyapi.co',
+ disabledHelp: 'Set Canopy API key in the provider configuration to enable this provider',
+ settingsClass: CanopySettings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::PRICE,
+ ],
+ expensive: true,
+ );
}
public function isActive(): bool
@@ -131,7 +135,7 @@ class CanopyProvider implements InfoProviderInterface
$dto = new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $result['asin'],
name: $result["title"],
description: "",
@@ -209,7 +213,7 @@ class CanopyProvider implements InfoProviderInterface
}
return new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $product['asin'],
name: $product['title'],
description: '',
@@ -222,12 +226,4 @@ class CanopyProvider implements InfoProviderInterface
);
}
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::PRICE,
- ];
- }
}
diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php
index 2e6708be..0fb1525f 100644
--- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php
@@ -27,6 +27,7 @@ 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\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\ConradSettings;
@@ -38,6 +39,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr
private const SEARCH_ENDPOINT = '/search/1/v3/facetSearch';
public const DISTRIBUTOR_NAME = 'Conrad';
+ public const PROVIDER_KEY = 'conrad';
private HttpClientInterface $httpClient;
@@ -51,20 +53,25 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr
]);
}
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'Conrad',
- 'description' => 'Retrieves part information from conrad.de',
- 'url' => 'https://www.conrad.de/',
- 'disabled_help' => 'Set API key in settings',
- 'settings_class' => ConradSettings::class,
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'conrad';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'Conrad',
+ description: 'Retrieves part information from conrad.de',
+ url: 'https://www.conrad.de/',
+ disabledHelp: 'Set API key in settings',
+ settingsClass: ConradSettings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::DATASHEET,
+ ProviderCapabilities::PRICE,
+ ProviderCapabilities::FOOTPRINT,
+ ProviderCapabilities::GTIN,
+ ProviderCapabilities::PARAMETERS
+ ],
+ );
}
public function isActive(): bool
@@ -111,7 +118,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr
foreach($results['hits'] as $result) {
$out[] = new SearchResultDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $result['productId'],
name: $result['manufacturerId'] ?? $result['productId'],
description: $result['title'] ?? '',
@@ -293,7 +300,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr
$data = $response->toArray();
return new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $data['shortProductNumber'],
name: $data['productFullInformation']['manufacturer']['name'] ?? $data['productFullInformation']['manufacturer']['id'] ?? $data['shortProductNumber'],
description: $data['productShortInformation']['title'] ?? '',
@@ -311,18 +318,6 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr
);
}
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::DATASHEET,
- ProviderCapabilities::PRICE,
- ProviderCapabilities::FOOTPRINT,
- ProviderCapabilities::GTIN,
- ];
- }
-
public function getHandledDomains(): array
{
$domains = [];
diff --git a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php
index e7a62aa4..fedda225 100644
--- a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php
@@ -29,6 +29,7 @@ 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\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Services\OAuth\OAuthTokenManager;
@@ -37,6 +38,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
class DigikeyProvider implements InfoProviderInterface
{
+ public const PROVIDER_KEY = 'digikey';
private const OAUTH_APP_NAME = 'ip_digikey_oauth';
@@ -72,32 +74,25 @@ class DigikeyProvider implements InfoProviderInterface
]);
}
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'DigiKey',
- 'description' => 'This provider uses the DigiKey API to search for parts.',
- 'url' => 'https://www.digikey.com/',
- 'oauth_app_name' => self::OAUTH_APP_NAME,
- 'disabled_help' => 'Set the Client ID and Secret in provider settings and connect OAuth to enable.',
- 'settings_class' => DigikeySettings::class,
- ];
- }
-
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::FOOTPRINT,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::DATASHEET,
- ProviderCapabilities::PRICE,
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'digikey';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'DigiKey',
+ description: 'This provider uses the DigiKey API to search for parts.',
+ url: 'https://www.digikey.com/',
+ disabledHelp: 'Set the Client ID and Secret in provider settings and connect OAuth to enable.',
+ oauthAppName: self::OAUTH_APP_NAME,
+ settingsClass: DigikeySettings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::FOOTPRINT,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::DATASHEET,
+ ProviderCapabilities::PRICE,
+ ProviderCapabilities::PARAMETERS
+ ],
+ );
}
public function isActive(): bool
@@ -128,7 +123,7 @@ class DigikeyProvider implements InfoProviderInterface
} catch (\InvalidArgumentException $exception) {
//Check if the exception was caused by an invalid or expired token
if (str_contains($exception->getMessage(), 'access_token')) {
- throw OAuthReconnectRequiredException::forProvider($this->getProviderKey());
+ throw OAuthReconnectRequiredException::forProvider(self::PROVIDER_KEY);
}
throw $exception;
@@ -141,7 +136,7 @@ class DigikeyProvider implements InfoProviderInterface
foreach ($products as $product) {
foreach ($product['ProductVariations'] as $variation) {
$result[] = new SearchResultDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $variation['DigiKeyProductNumber'],
name: $product['ManufacturerProductNumber'],
description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'],
@@ -168,7 +163,7 @@ class DigikeyProvider implements InfoProviderInterface
} catch (\InvalidArgumentException $exception) {
//Check if the exception was caused by an invalid or expired token
if (str_contains($exception->getMessage(), 'access_token')) {
- throw OAuthReconnectRequiredException::forProvider($this->getProviderKey());
+ throw OAuthReconnectRequiredException::forProvider(self::PROVIDER_KEY);
}
throw $exception;
@@ -191,7 +186,7 @@ class DigikeyProvider implements InfoProviderInterface
}
return new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $id,
name: $product['ManufacturerProductNumber'],
description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'],
diff --git a/src/Services/InfoProviderSystem/Providers/Element14Provider.php b/src/Services/InfoProviderSystem/Providers/Element14Provider.php
index 1d9e092c..ba8a7370 100644
--- a/src/Services/InfoProviderSystem/Providers/Element14Provider.php
+++ b/src/Services/InfoProviderSystem/Providers/Element14Provider.php
@@ -28,6 +28,7 @@ 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\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Settings\InfoProviderSystem\Element14Settings;
use Composer\CaBundle\CaBundle;
@@ -35,6 +36,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
class Element14Provider implements InfoProviderInterface, URLHandlerInfoProviderInterface
{
+ public const PROVIDER_KEY = 'element14';
private const ENDPOINT_URL = 'https://api.element14.com/catalog/products';
private const API_VERSION_NUMBER = '1.4';
@@ -60,20 +62,23 @@ class Element14Provider implements InfoProviderInterface, URLHandlerInfoProvider
]);
}
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'Farnell element14',
- 'description' => 'This provider uses the Farnell element14 API to search for parts.',
- 'url' => 'https://www.element14.com/',
- 'disabled_help' => 'Configure the API key in the provider settings to enable.',
- 'settings_class' => Element14Settings::class,
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'element14';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'Farnell element14',
+ description: 'This provider uses the Farnell element14 API to search for parts.',
+ url: 'https://www.element14.com/',
+ disabledHelp: 'Configure the API key in the provider settings to enable.',
+ settingsClass: Element14Settings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::DATASHEET,
+ ProviderCapabilities::PRICE,
+ ProviderCapabilities::PARAMETERS,
+ ],
+ );
}
public function isActive(): bool
@@ -113,7 +118,7 @@ class Element14Provider implements InfoProviderInterface, URLHandlerInfoProvider
foreach ($products as $product) {
$result[] = new PartDetailDTO(
- provider_key: $this->getProviderKey(), provider_id: $product['sku'],
+ provider_key: self::PROVIDER_KEY, provider_id: $product['sku'],
name: $product['translatedManufacturerPartNumber'],
description: $this->displayNameToDescription($product['displayName'], $product['translatedManufacturerPartNumber']),
manufacturer: $product['vendorName'] ?? $product['brandName'] ?? null,
@@ -301,15 +306,6 @@ class Element14Provider implements InfoProviderInterface, URLHandlerInfoProvider
return $tmp[0];
}
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::DATASHEET,
- ];
- }
-
public function getHandledDomains(): array
{
return ['element14.com', 'farnell.com', 'newark.com'];
diff --git a/src/Services/InfoProviderSystem/Providers/EmptyProvider.php b/src/Services/InfoProviderSystem/Providers/EmptyProvider.php
index 915a118c..eea14ed8 100644
--- a/src/Services/InfoProviderSystem/Providers/EmptyProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/EmptyProvider.php
@@ -25,6 +25,7 @@ namespace App\Services\InfoProviderSystem\Providers;
use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
+use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use Symfony\Component\DependencyInjection\Attribute\When;
@@ -34,19 +35,20 @@ use Symfony\Component\DependencyInjection\Attribute\When;
#[When(env: 'test')]
class EmptyProvider implements InfoProviderInterface
{
- public function getProviderInfo(): array
- {
- return [
- 'name' => 'Empty Provider',
- 'description' => 'This is a test provider',
- //'url' => 'https://example.com',
- 'disabled_help' => 'This provider is disabled for testing purposes'
- ];
- }
+ public const PROVIDER_KEY = 'empty';
- public function getProviderKey(): string
+ public function getProviderInfo(): ProviderInfoDTO
{
- return 'empty';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'Empty Provider',
+ description: 'This is a test provider',
+ disabledHelp: 'This provider is disabled for testing purposes',
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::FOOTPRINT,
+ ],
+ );
}
public function isActive(): bool
@@ -61,14 +63,6 @@ class EmptyProvider implements InfoProviderInterface
];
}
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::FOOTPRINT,
- ];
- }
-
public function getDetails(string $id, array $options = []): PartDetailDTO
{
throw new \RuntimeException('No part details available');
diff --git a/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php b/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php
index 45777f9e..420049b3 100644
--- a/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php
@@ -30,6 +30,7 @@ use App\Services\InfoProviderSystem\CreateFromUrlHelper;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
+use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Services\InfoProviderSystem\PartInfoRetriever;
@@ -53,6 +54,7 @@ class GenericWebProvider implements InfoProviderInterface
use FixAndValidateUrlTrait;
public const DISTRIBUTOR_NAME = 'Website';
+ public const PROVIDER_KEY = 'generic_web';
private readonly HttpClientInterface $httpClient;
@@ -69,20 +71,21 @@ class GenericWebProvider implements InfoProviderInterface
);
}
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'Generic Web URL',
- 'description' => 'Tries to extract a part from a given product webpage URL using common metadata standards like JSON-LD and OpenGraph.',
- //'url' => 'https://example.com',
- 'disabled_help' => 'Enable in settings to use this provider',
- 'settings_class' => GenericWebProviderSettings::class,
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'generic_web';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'Generic Web URL',
+ description: 'Tries to extract a part from a given product webpage URL using common metadata standards like JSON-LD and OpenGraph.',
+ disabledHelp: 'Enable in settings to use this provider',
+ settingsClass: GenericWebProviderSettings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::PRICE,
+ ProviderCapabilities::GTIN,
+ ],
+ );
}
public function isActive(): bool
@@ -227,7 +230,7 @@ class GenericWebProvider implements InfoProviderInterface
}
return new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $url,
name: $product->name?->toString() ?? $product->alternateName?->toString() ?? $product->mpn?->toString() ?? 'Unknown Name',
description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '',
@@ -376,7 +379,7 @@ class GenericWebProvider implements InfoProviderInterface
)];
return new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $canonicalURL,
name: $this->getMetaContent($dom, 'og:title') ?? $pageTitle,
description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '',
@@ -387,13 +390,4 @@ class GenericWebProvider implements InfoProviderInterface
);
}
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::PRICE,
- ProviderCapabilities::GTIN,
- ];
- }
}
diff --git a/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php b/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php
index d3895795..cdc8086c 100644
--- a/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php
+++ b/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php
@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace App\Services\InfoProviderSystem\Providers;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
+use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
interface InfoProviderInterface
@@ -34,26 +35,8 @@ interface InfoProviderInterface
/**
* Get information about this provider
- *
- * @return array An associative array with the following keys (? means optional):
- * - name: The (user friendly) name of the provider (e.g. "Digikey"), will be translated
- * - description?: A short description of the provider (e.g. "Digikey is a ..."), will be translated
- * - logo?: The logo of the provider (e.g. "digikey.png")
- * - url?: The url of the provider (e.g. "https://www.digikey.com")
- * - disabled_help?: A help text which is shown when the provider is disabled, explaining how to enable it
- * - oauth_app_name?: The name of the OAuth app which is used for authentication (e.g. "ip_digikey_oauth"). If this is set a connect button will be shown
- * - settings_class?: The class name of the settings class which contains the settings for this provider (e.g. "App\Settings\InfoProviderSettings\DigikeySettings"). If this is set a link to the settings will be shown
- *
- * @phpstan-return array{ name: string, description?: string, logo?: string, url?: string, disabled_help?: string, oauth_app_name?: string, settings_class?: class-string }
*/
- public function getProviderInfo(): array;
-
- /**
- * Returns a unique key for this provider, which will be saved into the database
- * and used to identify the provider
- * @return string A unique key for this provider (e.g. "digikey")
- */
- public function getProviderKey(): string;
+ public function getProviderInfo(): ProviderInfoDTO;
/**
* Checks if this provider is enabled or not (meaning that it can be used for searching)
@@ -76,12 +59,4 @@ interface InfoProviderInterface
* @return PartDetailDTO
*/
public function getDetails(string $id, array $options = []): PartDetailDTO;
-
- /**
- * A list of capabilities this provider supports (which kind of data it can provide).
- * Not every part have to contain all of these data, but the provider should be able to provide them in general.
- * Currently, this list is purely informational and not used in functional checks.
- * @return ProviderCapabilities[]
- */
- public function getCapabilities(): array;
}
diff --git a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php
index b3da3cda..de1906cc 100755
--- a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php
@@ -28,6 +28,7 @@ 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\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Settings\InfoProviderSystem\LCSCSettings;
use Symfony\Component\HttpFoundation\Cookie;
@@ -39,26 +40,31 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider
private const ENDPOINT_URL = 'https://wmsc.lcsc.com/ftps/wm';
public const DISTRIBUTOR_NAME = 'LCSC';
+ public const PROVIDER_KEY = 'lcsc';
public function __construct(private readonly HttpClientInterface $lcscClient, private readonly LCSCSettings $settings)
{
}
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'LCSC',
- 'description' => 'This provider uses the (unofficial) LCSC API to search for parts.',
- 'url' => 'https://www.lcsc.com/',
- 'disabled_help' => 'Enable this provider in the provider settings.',
- 'settings_class' => LCSCSettings::class,
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'lcsc';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'LCSC',
+ description: 'This provider uses the (unofficial) LCSC API to search for parts.',
+ url: 'https://www.lcsc.com/',
+ disabledHelp: 'Enable this provider in the provider settings.',
+ settingsClass: LCSCSettings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::DATASHEET,
+ ProviderCapabilities::PRICE,
+ ProviderCapabilities::FOOTPRINT,
+ ProviderCapabilities::PARAMETERS
+ ],
+ );
}
// This provider is always active
@@ -227,7 +233,7 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider
}
return new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $product['productCode'],
name: $product['productModel'],
description: $this->sanitizeField($product['productIntroEn']) ?? '',
@@ -455,17 +461,6 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider
return $tmp[0];
}
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::DATASHEET,
- ProviderCapabilities::PRICE,
- ProviderCapabilities::FOOTPRINT,
- ];
- }
-
public function getHandledDomains(): array
{
return ['lcsc.com'];
diff --git a/src/Services/InfoProviderSystem/Providers/MouserProvider.php b/src/Services/InfoProviderSystem/Providers/MouserProvider.php
index 49ca2d50..c1b91faa 100644
--- a/src/Services/InfoProviderSystem/Providers/MouserProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/MouserProvider.php
@@ -36,6 +36,7 @@ use App\Entity\Parts\ManufacturingStatus;
use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
+use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Settings\InfoProviderSystem\MouserSettings;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -48,6 +49,7 @@ class MouserProvider implements InfoProviderInterface
private const ENDPOINT_URL = 'https://api.mouser.com/api/v2/search';
public const DISTRIBUTOR_NAME = 'Mouser';
+ public const PROVIDER_KEY = 'mouser';
public function __construct(
private readonly HttpClientInterface $mouserClient,
@@ -55,20 +57,22 @@ class MouserProvider implements InfoProviderInterface
) {
}
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'Mouser',
- 'description' => 'This provider uses the Mouser API to search for parts.',
- 'url' => 'https://www.mouser.com/',
- 'disabled_help' => 'Configure the API key in the provider settings to enable.',
- 'settings_class' => MouserSettings::class
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'mouser';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'Mouser',
+ description: 'This provider uses the Mouser API to search for parts.',
+ url: 'https://www.mouser.com/',
+ disabledHelp: 'Configure the API key in the provider settings to enable.',
+ settingsClass: MouserSettings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::DATASHEET,
+ ProviderCapabilities::PRICE,
+ ],
+ );
}
public function isActive(): bool
@@ -207,17 +211,6 @@ class MouserProvider implements InfoProviderInterface
return reset($tmp);
}
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::DATASHEET,
- ProviderCapabilities::PRICE,
- ];
- }
-
-
/**
* @param ResponseInterface $response
* @return PartDetailDTO[]
@@ -251,7 +244,7 @@ class MouserProvider implements InfoProviderInterface
$result[] = new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $product['MouserPartNumber'],
name: $product['ManufacturerPartNumber'],
description: $product['Description'],
diff --git a/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php b/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php
index 9764517b..edeb5f71 100644
--- a/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php
@@ -88,6 +88,7 @@ use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
+use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Settings\InfoProviderSystem\OEMSecretsSettings;
use App\Settings\InfoProviderSystem\OEMSecretsSortMode;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -96,6 +97,7 @@ use Psr\Cache\CacheItemPoolInterface;
class OEMSecretsProvider implements InfoProviderInterface
{
+ public const PROVIDER_KEY = 'oemsecrets';
private const ENDPOINT_URL = 'https://oemsecretsapi.com/partsearch';
@@ -227,37 +229,23 @@ class OEMSecretsProvider implements InfoProviderInterface
private array $distributorCountryCodes = [];
private array $countryCodeToRegionMap = [];
- /**
- * Get information about this provider
- *
- * @return array An associative array with the following keys (? means optional):
- * - name: The (user friendly) name of the provider (e.g. "Digikey"), will be translated
- * - description?: A short description of the provider (e.g. "Digikey is a ..."), will be translated
- * - logo?: The logo of the provider (e.g. "digikey.png")
- * - url?: The url of the provider (e.g. "https://www.digikey.com")
- * - disabled_help?: A help text which is shown when the provider is disabled, explaining how to enable it
- * - oauth_app_name?: The name of the OAuth app which is used for authentication (e.g. "ip_digikey_oauth"). If this is set a connect button will be shown
- *
- * @phpstan-return array{ name: string, description?: string, logo?: string, url?: string, disabled_help?: string, oauth_app_name?: string }
- */
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'OEMSecrets',
- 'description' => 'This provider uses the OEMSecrets API to search for parts.',
- 'url' => 'https://www.oemsecrets.com/',
- 'disabled_help' => 'Configure the API key in the provider settings to enable.',
- 'settings_class' => OEMSecretsSettings::class
- ];
- }
- /**
- * Returns a unique key for this provider, which will be saved into the database
- * and used to identify the provider
- * @return string A unique key for this provider (e.g. "digikey")
- */
- public function getProviderKey(): string
- {
- return 'oemsecrets';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'OEMSecrets',
+ description: 'This provider uses the OEMSecrets API to search for parts.',
+ url: 'https://www.oemsecrets.com/',
+ disabledHelp: 'Configure the API key in the provider settings to enable.',
+ settingsClass: OEMSecretsSettings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::DATASHEET,
+ ProviderCapabilities::PRICE,
+ ProviderCapabilities::PARAMETERS
+ ],
+ );
}
/**
@@ -456,17 +444,6 @@ class OEMSecretsProvider implements InfoProviderInterface
* Currently, this list is purely informational and not used in functional checks.
* @return ProviderCapabilities[]
*/
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::DATASHEET,
- ProviderCapabilities::PRICE,
- ];
- }
-
-
/**
* Processes a single product and updates arrays for basic information, datasheets, images, parameters,
* and purchase information. Aggregates and organizes data received for a specific `part_number` and `manufacturer_id`.
@@ -896,7 +873,7 @@ class OEMSecretsProvider implements InfoProviderInterface
// If there is no existing basic info array, we create a new one
if (is_null($existingBasicInfo)) {
return [
- 'provider_key' => $this->getProviderKey(),
+ 'provider_key' => self::PROVIDER_KEY,
'provider_id' => $provider_id,
'name' => $product['part_number'],
'description' => $description,
@@ -916,7 +893,7 @@ class OEMSecretsProvider implements InfoProviderInterface
// Update fields only if empty or undefined, with additional check for preview_image_url
return [
- 'provider_key' => $existingBasicInfo['provider_key'] ?? $this->getProviderKey(),
+ 'provider_key' => $existingBasicInfo['provider_key'] ?? self::PROVIDER_KEY,
'provider_id' => $existingBasicInfo['provider_id'] ?? $provider_id,
'name' => $existingBasicInfo['name'] ?? $product['part_number'],
// Update description if it's null/empty
@@ -1270,7 +1247,7 @@ class OEMSecretsProvider implements InfoProviderInterface
*/
private function generateInquiryUrl(string $partNumber, string $oemInquiry = 'compare/'): string
{
- $baseUrl = rtrim($this->getProviderInfo()['url'], '/') . '/';
+ $baseUrl = rtrim($this->getProviderInfo()->url, '/') . '/';
$inquiryPath = trim($oemInquiry, '/') . '/';
$encodedPartNumber = urlencode(trim($partNumber));
return $baseUrl . $inquiryPath . $encodedPartNumber;
diff --git a/src/Services/InfoProviderSystem/Providers/OctopartProvider.php b/src/Services/InfoProviderSystem/Providers/OctopartProvider.php
index de404e18..14fdbfa5 100644
--- a/src/Services/InfoProviderSystem/Providers/OctopartProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/OctopartProvider.php
@@ -28,6 +28,7 @@ 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\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\OAuth\OAuthTokenManager;
use App\Settings\InfoProviderSystem\OctopartSettings;
@@ -43,6 +44,8 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*/
class OctopartProvider implements InfoProviderInterface
{
+ public const PROVIDER_KEY = 'octopart';
+
private const OAUTH_APP_NAME = 'ip_octopart_oauth';
/**
@@ -164,20 +167,25 @@ class OctopartProvider implements InfoProviderInterface
return $response->toArray(true);
}
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'Octopart',
- 'description' => 'This provider uses the Nexar/Octopart API to search for parts on Octopart.',
- 'url' => 'https://www.octopart.com/',
- 'disabled_help' => 'Set the Client ID and Secret in provider settings.',
- 'settings_class' => OctopartSettings::class
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'octopart';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'Octopart',
+ description: 'This provider uses the Nexar/Octopart API to search for parts on Octopart.',
+ url: 'https://www.octopart.com/',
+ disabledHelp: 'Set the Client ID and Secret in provider settings.',
+ settingsClass: OctopartSettings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::FOOTPRINT,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::DATASHEET,
+ ProviderCapabilities::PRICE,
+ ProviderCapabilities::PARAMETERS
+ ],
+ expensive: true,
+ );
}
public function isActive(): bool
@@ -307,7 +315,7 @@ class OctopartProvider implements InfoProviderInterface
}
return new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $part['id'],
name: $part['mpn'],
description: $part['shortDescription'] ?? null,
@@ -397,14 +405,4 @@ class OctopartProvider implements InfoProviderInterface
return $tmp;
}
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::FOOTPRINT,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::DATASHEET,
- ProviderCapabilities::PRICE,
- ];
- }
}
diff --git a/src/Services/InfoProviderSystem/Providers/PollinProvider.php b/src/Services/InfoProviderSystem/Providers/PollinProvider.php
index 7acecc3a..919db66b 100644
--- a/src/Services/InfoProviderSystem/Providers/PollinProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/PollinProvider.php
@@ -29,6 +29,7 @@ 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\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\PollinSettings;
@@ -38,6 +39,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInterface
{
+ public const PROVIDER_KEY = 'pollin';
public function __construct(private readonly HttpClientInterface $client,
private readonly PollinSettings $settings,
@@ -45,20 +47,23 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt
{
}
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'Pollin',
- 'description' => 'Webscraping from pollin.de to get part information',
- 'url' => 'https://www.pollin.de/',
- 'disabled_help' => 'Enable the provider in provider settings',
- 'settings_class' => PollinSettings::class,
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'pollin';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'Pollin',
+ description: 'Webscraping from pollin.de to get part information',
+ url: 'https://www.pollin.de/',
+ disabledHelp: 'Enable the provider in provider settings',
+ settingsClass: PollinSettings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::PRICE,
+ ProviderCapabilities::DATASHEET,
+ ProviderCapabilities::PARAMETERS
+ ],
+ );
}
public function isActive(): bool
@@ -88,7 +93,7 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt
//Iterate over each div.product-box
$dom->filter('div.product-box')->each(function (Crawler $node) use (&$results) {
$results[] = new SearchResultDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $node->filter('meta[itemprop="productID"]')->attr('content'),
name: $node->filter('a.product-name')->text(),
description: '',
@@ -156,7 +161,7 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt
$purchaseInfo = new PurchaseInfoDTO('Pollin', $orderId, $this->parsePrices($dom), $productPageUrl);
return new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $orderId,
name: trim($dom->filter('meta[property="og:title"]')->attr('content')),
description: $dom->filter('meta[property="og:description"]')->attr('content'),
@@ -244,16 +249,6 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt
];
}
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::PRICE,
- ProviderCapabilities::DATASHEET
- ];
- }
-
public function getHandledDomains(): array
{
return ['pollin.de'];
diff --git a/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php b/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php
index 3a7d03e9..a97aa347 100644
--- a/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php
+++ b/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php
@@ -26,28 +26,28 @@ namespace App\Services\InfoProviderSystem\Providers;
/**
* This enum contains all capabilities (which data it can provide) a provider can have.
*/
-enum ProviderCapabilities
+enum ProviderCapabilities: string
{
/** Basic information about a part, like the name, description, part number, manufacturer etc */
- case BASIC;
+ case BASIC = 'BASIC';
/** Provider can provide a picture for a part */
- case PICTURE;
+ case PICTURE = 'PICTURE';
/** Provider can provide datasheets for a part */
- case DATASHEET;
+ case DATASHEET = 'DATASHEET';
/** Provider can provide prices for a part */
- case PRICE;
+ case PRICE = 'PRICE';
/** Information about the footprint of a part */
- case FOOTPRINT;
+ case FOOTPRINT = 'FOOTPRINT';
/** Provider can provide GTIN for a part */
- case GTIN;
+ case GTIN = 'GTIN';
/** Provider can provide parameters/specifications for a part */
- case PARAMETERS;
+ case PARAMETERS = 'PARAMETERS';
/**
* Get the order index for displaying capabilities in a stable order.
diff --git a/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php b/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php
index 9dfb099f..901956e1 100644
--- a/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php
@@ -28,6 +28,7 @@ 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\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\ReicheltSettings;
@@ -38,6 +39,7 @@ class ReicheltProvider implements InfoProviderInterface
{
public const DISTRIBUTOR_NAME = "Reichelt";
+ public const PROVIDER_KEY = 'reichelt';
private readonly HttpClientInterface $client;
@@ -48,20 +50,24 @@ class ReicheltProvider implements InfoProviderInterface
$this->client = new RandomizeUseragentHttpClient($client);
}
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'Reichelt',
- 'description' => 'Webscraping from reichelt.com to get part information',
- 'url' => 'https://www.reichelt.com/',
- 'disabled_help' => 'Enable provider in provider settings.',
- 'settings_class' => ReicheltSettings::class,
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'reichelt';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'Reichelt',
+ description: 'Webscraping from reichelt.com to get part information',
+ url: 'https://www.reichelt.com/',
+ disabledHelp: 'Enable provider in provider settings.',
+ settingsClass: ReicheltSettings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::DATASHEET,
+ ProviderCapabilities::PRICE,
+ ProviderCapabilities::GTIN,
+ ProviderCapabilities::PARAMETERS
+ ],
+ );
}
public function isActive(): bool
@@ -93,7 +99,7 @@ class ReicheltProvider implements InfoProviderInterface
$pictureURL = $element->filter("div.al_artlogo img")->attr('src');
$results[] = new SearchResultDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $artId,
name: $productID,
description: $name,
@@ -173,7 +179,7 @@ class ReicheltProvider implements InfoProviderInterface
//Create part object
return new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $id,
name: $json[0]['article_artnr'],
description: $json[0]['article_besch'],
@@ -282,14 +288,4 @@ class ReicheltProvider implements InfoProviderInterface
return 'https://www.reichelt.com/' . strtolower($this->settings->country) . '/' . strtolower($this->settings->language);
}
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::DATASHEET,
- ProviderCapabilities::PRICE,
- ProviderCapabilities::GTIN,
- ];
- }
}
diff --git a/src/Services/InfoProviderSystem/Providers/TMEProvider.php b/src/Services/InfoProviderSystem/Providers/TMEProvider.php
index 24ba0ea7..3d230a26 100644
--- a/src/Services/InfoProviderSystem/Providers/TMEProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/TMEProvider.php
@@ -28,12 +28,14 @@ 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\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\TMESettings;
class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterface
{
+ public const PROVIDER_KEY = 'tme';
private const VENDOR_NAME = 'TME';
@@ -48,20 +50,24 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
}
}
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'TME',
- 'description' => 'This provider uses the API of TME (Transfer Multipart).',
- 'url' => 'https://tme.eu/',
- 'disabled_help' => 'Configure the API Token and secret in provider settings to use this provider.',
- 'settings_class' => TMESettings::class
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'tme';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'TME',
+ description: 'This provider uses the API of TME (Transfer Multipart).',
+ url: 'https://tme.eu/',
+ disabledHelp: 'Configure the API Token and secret in provider settings to use this provider.',
+ settingsClass: TMESettings::class,
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::FOOTPRINT,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::DATASHEET,
+ ProviderCapabilities::PRICE,
+ ProviderCapabilities::PARAMETERS
+ ],
+ );
}
public function isActive(): bool
@@ -83,7 +89,7 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
foreach($data['ProductList'] as $product) {
$result[] = new SearchResultDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $product['Symbol'],
name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'],
description: $product['Description'],
@@ -119,7 +125,7 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
$parameters = $this->getParameters($id, $footprint);
return new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $product['Symbol'],
name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'],
description: $product['Description'],
@@ -290,17 +296,6 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
return $url;
}
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::FOOTPRINT,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::DATASHEET,
- ProviderCapabilities::PRICE,
- ];
- }
-
public function getHandledDomains(): array
{
return ['tme.eu'];
diff --git a/src/Services/InfoProviderSystem/Providers/TestProvider.php b/src/Services/InfoProviderSystem/Providers/TestProvider.php
index 42927abd..f4528d34 100644
--- a/src/Services/InfoProviderSystem/Providers/TestProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/TestProvider.php
@@ -25,6 +25,7 @@ namespace App\Services\InfoProviderSystem\Providers;
use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
+use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use Symfony\Component\DependencyInjection\Attribute\When;
@@ -34,20 +35,20 @@ use Symfony\Component\DependencyInjection\Attribute\When;
#[When(env: 'test')]
class TestProvider implements InfoProviderInterface
{
+ public const PROVIDER_KEY = 'test';
- public function getProviderInfo(): array
+ public function getProviderInfo(): ProviderInfoDTO
{
- return [
- 'name' => 'Test Provider',
- 'description' => 'This is a test provider',
- //'url' => 'https://example.com',
- 'disabled_help' => 'This provider is disabled for testing purposes'
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'test';
+ return new ProviderInfoDTO(
+ key: self::PROVIDER_KEY,
+ name: 'Test Provider',
+ description: 'This is a test provider',
+ disabledHelp: 'This provider is disabled for testing purposes',
+ capabilities: [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::FOOTPRINT,
+ ],
+ );
}
public function isActive(): bool
@@ -58,24 +59,16 @@ class TestProvider implements InfoProviderInterface
public function searchByKeyword(string $keyword, array $options = []): array
{
return [
- new SearchResultDTO(provider_key: $this->getProviderKey(), provider_id: 'element1', name: 'Element 1', description: 'fd'),
- new SearchResultDTO(provider_key: $this->getProviderKey(), provider_id: 'element2', name: 'Element 2', description: 'fd'),
- new SearchResultDTO(provider_key: $this->getProviderKey(), provider_id: 'element3', name: 'Element 3', description: 'fd'),
- ];
- }
-
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::FOOTPRINT,
+ new SearchResultDTO(provider_key: self::PROVIDER_KEY, provider_id: 'element1', name: 'Element 1', description: 'fd'),
+ new SearchResultDTO(provider_key: self::PROVIDER_KEY, provider_id: 'element2', name: 'Element 2', description: 'fd'),
+ new SearchResultDTO(provider_key: self::PROVIDER_KEY, provider_id: 'element3', name: 'Element 3', description: 'fd'),
];
}
public function getDetails(string $id, array $options = []): PartDetailDTO
{
return new PartDetailDTO(
- provider_key: $this->getProviderKey(),
+ provider_key: self::PROVIDER_KEY,
provider_id: $id,
name: 'Test Element',
description: 'fd',
diff --git a/src/Settings/AISettings/AISettings.php b/src/Settings/AISettings/AISettings.php
index 659577b6..79abfda9 100644
--- a/src/Settings/AISettings/AISettings.php
+++ b/src/Settings/AISettings/AISettings.php
@@ -37,6 +37,9 @@ class AISettings
public const TIMEOUT_LIMIT = 600;
+ #[EmbeddedSettings]
+ public ?McpSettings $mcp = null;
+
#[EmbeddedSettings]
public ?OpenRouterSettings $openRouter = null;
diff --git a/src/Settings/AISettings/McpSettings.php b/src/Settings/AISettings/McpSettings.php
new file mode 100644
index 00000000..929efa41
--- /dev/null
+++ b/src/Settings/AISettings/McpSettings.php
@@ -0,0 +1,45 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\Settings\AISettings;
+
+use App\Settings\SettingsIcon;
+use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
+use Jbtronics\SettingsBundle\Settings\Settings;
+use Jbtronics\SettingsBundle\Settings\SettingsParameter;
+use Jbtronics\SettingsBundle\Settings\SettingsTrait;
+use Symfony\Component\Translation\TranslatableMessage as TM;
+
+#[Settings(label: new TM("settings.misc.mcp"))]
+#[SettingsIcon("fa-robot")]
+class McpSettings
+{
+ use SettingsTrait;
+
+ #[SettingsParameter(
+ label: new TM("settings.misc.mcp.enabled"),
+ description: new TM("settings.misc.mcp.enabled.help"),
+ envVar: "bool:MCP_ENABLED",
+ envVarMode: EnvVarMode::OVERWRITE,
+ )]
+ public bool $enabled = false;
+}
diff --git a/src/State/Mcp/GetInfoProviderPartDetailsProcessor.php b/src/State/Mcp/GetInfoProviderPartDetailsProcessor.php
new file mode 100644
index 00000000..c7949256
--- /dev/null
+++ b/src/State/Mcp/GetInfoProviderPartDetailsProcessor.php
@@ -0,0 +1,51 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\State\Mcp;
+
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProcessorInterface;
+use App\Exceptions\InfoProviderNotActiveException;
+use App\Mcp\DTO\InfoProviderPartDetailsInput;
+use App\Services\InfoProviderSystem\PartInfoRetriever;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+class GetInfoProviderPartDetailsProcessor implements ProcessorInterface
+{
+ public function __construct(
+ private readonly PartInfoRetriever $infoRetriever,
+ ) {
+ }
+
+ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
+ {
+ if (!$data instanceof InfoProviderPartDetailsInput) {
+ throw new BadRequestHttpException('Expected InfoProviderPartDetailsInput');
+ }
+
+ try {
+ return $this->infoRetriever->getDetails($data->provider_key, $data->provider_id);
+ } catch (InfoProviderNotActiveException|\InvalidArgumentException $e) {
+ throw new BadRequestHttpException($e->getMessage(), $e);
+ }
+ }
+}
diff --git a/src/State/Mcp/GetPartByIdProcessor.php b/src/State/Mcp/GetPartByIdProcessor.php
new file mode 100644
index 00000000..b147ef68
--- /dev/null
+++ b/src/State/Mcp/GetPartByIdProcessor.php
@@ -0,0 +1,60 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\State\Mcp;
+
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProcessorInterface;
+use App\Entity\Parts\Part;
+use App\Mcp\DTO\ElementByIdInput;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+use Symfony\Component\Security\Core\Exception\AccessDeniedException;
+
+class GetPartByIdProcessor implements ProcessorInterface
+{
+ public function __construct(
+ private readonly EntityManagerInterface $entityManager,
+ private readonly AuthorizationCheckerInterface $authorizationChecker,
+ ) {
+ }
+
+ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
+ {
+ if (!$data instanceof ElementByIdInput) {
+ throw new \InvalidArgumentException('Expected PartByIdInput');
+ }
+
+ $part = $this->entityManager->find(Part::class, $data->id);
+
+ if (!$part instanceof Part) {
+ throw new NotFoundHttpException(sprintf('Part with id %d not found', $data->id));
+ }
+
+ if (!$this->authorizationChecker->isGranted('read', $part)) {
+ throw new AccessDeniedException(sprintf('Access denied to part with id %d', $data->id));
+ }
+
+ return $part;
+ }
+}
diff --git a/src/State/Mcp/GetStructuralElementDetailsProcessor.php b/src/State/Mcp/GetStructuralElementDetailsProcessor.php
new file mode 100644
index 00000000..27cab45a
--- /dev/null
+++ b/src/State/Mcp/GetStructuralElementDetailsProcessor.php
@@ -0,0 +1,70 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\State\Mcp;
+
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProcessorInterface;
+use App\Entity\Base\AbstractStructuralDBElement;
+use App\Mcp\DTO\ElementByIdInput;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+use Symfony\Component\Security\Core\Exception\AccessDeniedException;
+
+/**
+ * Generic get-by-id processor shared by all structural "master data" entities (categories, footprints,
+ * manufacturers, storage locations, suppliers, measurement units, part custom states). The concrete entity
+ * class is determined from the operation, so this single processor can be reused for all of them.
+ */
+class GetStructuralElementDetailsProcessor implements ProcessorInterface
+{
+ public function __construct(
+ private readonly EntityManagerInterface $entityManager,
+ private readonly AuthorizationCheckerInterface $authorizationChecker,
+ ) {
+ }
+
+ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): AbstractStructuralDBElement
+ {
+ if (!$data instanceof ElementByIdInput) {
+ throw new \InvalidArgumentException('Expected ElementByIdInput');
+ }
+
+ $class = $operation->getClass();
+ if (!is_a($class, AbstractStructuralDBElement::class, true)) {
+ throw new \LogicException(sprintf('%s can only be used for resources extending %s', self::class, AbstractStructuralDBElement::class));
+ }
+
+ $element = $this->entityManager->find($class, $data->id);
+
+ if (!$element instanceof AbstractStructuralDBElement) {
+ throw new NotFoundHttpException(sprintf('%s with id %d not found', (new \ReflectionClass($class))->getShortName(), $data->id));
+ }
+
+ if (!$this->authorizationChecker->isGranted('read', $element)) {
+ throw new AccessDeniedException(sprintf('Access denied to %s with id %d', (new \ReflectionClass($class))->getShortName(), $data->id));
+ }
+
+ return $element;
+ }
+}
diff --git a/src/State/Mcp/ListInfoProvidersProcessor.php b/src/State/Mcp/ListInfoProvidersProcessor.php
new file mode 100644
index 00000000..6b995a15
--- /dev/null
+++ b/src/State/Mcp/ListInfoProvidersProcessor.php
@@ -0,0 +1,71 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\State\Mcp;
+
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProcessorInterface;
+use ApiPlatform\State\ProviderInterface;
+use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
+use App\Services\InfoProviderSystem\ProviderRegistry;
+
+/**
+ * Used both as the state processor for the MCP list_info_providers tool and as the state provider for the
+ * REST GET /api/info_providers collection endpoint.
+ */
+class ListInfoProvidersProcessor implements ProcessorInterface, ProviderInterface
+{
+ public function __construct(
+ private readonly ProviderRegistry $providerRegistry,
+ ) {
+ }
+
+ /**
+ * @return ProviderInfoDTO[]
+ */
+ public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
+ {
+ return $this->listActiveProviders();
+ }
+
+ /**
+ * @return ProviderInfoDTO[]
+ */
+ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): array
+ {
+ return $this->listActiveProviders();
+ }
+
+ /**
+ * @return ProviderInfoDTO[]
+ */
+ private function listActiveProviders(): array
+ {
+ $result = [];
+
+ foreach ($this->providerRegistry->getActiveProviders() as $provider) {
+ $result[] = $provider->getProviderInfo();
+ }
+
+ return $result;
+ }
+}
diff --git a/src/State/Mcp/ListStructuralElementsProcessor.php b/src/State/Mcp/ListStructuralElementsProcessor.php
new file mode 100644
index 00000000..115312b4
--- /dev/null
+++ b/src/State/Mcp/ListStructuralElementsProcessor.php
@@ -0,0 +1,98 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\State\Mcp;
+
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProcessorInterface;
+use App\Entity\Base\AbstractStructuralDBElement;
+use App\Mcp\DTO\StructuralElementOverview;
+use App\Mcp\DTO\StructuralElementSearchInput;
+use App\Services\Trees\NodesListBuilder;
+use Doctrine\ORM\EntityManagerInterface;
+
+/**
+ * Generic list/search processor shared by all structural "master data" entities (categories, footprints,
+ * manufacturers, storage locations, suppliers, measurement units, part custom states). The concrete entity
+ * class is determined from the operation, so this single processor can be reused for all of them.
+ * Returns a lean id+name+full_path overview only; use the corresponding get_X_details tool for full details.
+ */
+readonly class ListStructuralElementsProcessor implements ProcessorInterface
+{
+ public function __construct(
+ private EntityManagerInterface $entityManager,
+ private NodesListBuilder $nodesListBuilder,
+ ) {
+ }
+
+ /**
+ * @return StructuralElementOverview[]
+ */
+ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): array
+ {
+ if (!$data instanceof StructuralElementSearchInput) {
+ throw new \InvalidArgumentException('Expected StructuralElementSearchInput');
+ }
+
+ $class = $operation->getClass();
+ if (!is_a($class, AbstractStructuralDBElement::class, true)) {
+ throw new \LogicException(sprintf('%s can only be used for resources extending %s', self::class, AbstractStructuralDBElement::class));
+ }
+
+ if ($data->keyword === null || $data->keyword === '') {
+ //Without a filter, use the (cached) NodesListBuilder, which already returns the elements in
+ //correct hierarchical tree order (parents immediately followed by their own children), instead
+ //of fetching everything and re-sorting it ourselves.
+ /** @var AbstractStructuralDBElement[] $elements */
+ $elements = $this->nodesListBuilder->typeToNodesList($class);
+
+ return array_map($this->toOverview(...), $elements);
+ }
+
+ $qb = $this->entityManager->getRepository($class)->createQueryBuilder('element');
+
+ //Escape % and _ characters in the keyword, like PartSearchFilter does
+ $keyword = str_replace(['%', '_'], ['\%', '\_'], $data->keyword);
+ $qb->andWhere('ILIKE(element.name, :keyword) = TRUE OR ILIKE(element.comment, :keyword) = TRUE')
+ ->setParameter('keyword', '%'.$keyword.'%');
+
+ /** @var AbstractStructuralDBElement[] $elements */
+ $elements = $qb->getQuery()->getResult();
+
+ $overviews = array_map($this->toOverview(...), $elements);
+
+ //Sort by full path (rather than the DB query), so that parents are always immediately followed by
+ //their own children, allowing clients to derive the hierarchical structure from a flat list.
+ usort($overviews, static fn (StructuralElementOverview $a, StructuralElementOverview $b): int => strnatcasecmp($a->full_path, $b->full_path));
+
+ return $overviews;
+ }
+
+ private function toOverview(AbstractStructuralDBElement $element): StructuralElementOverview
+ {
+ return new StructuralElementOverview(
+ id: $element->getID(),
+ name: $element->getName(),
+ full_path: $element->getFullPath(),
+ );
+ }
+}
diff --git a/src/State/Mcp/SearchInfoProvidersProcessor.php b/src/State/Mcp/SearchInfoProvidersProcessor.php
new file mode 100644
index 00000000..6cd76145
--- /dev/null
+++ b/src/State/Mcp/SearchInfoProvidersProcessor.php
@@ -0,0 +1,90 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\State\Mcp;
+
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProcessorInterface;
+use App\Exceptions\InfoProviderNotActiveException;
+use App\Mcp\DTO\InfoProviderSearchInput;
+use App\Services\InfoProviderSystem\PartInfoRetriever;
+use App\Services\InfoProviderSystem\ProviderRegistry;
+use App\Settings\InfoProviderSystem\InfoProviderGeneralSettings;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+class SearchInfoProvidersProcessor implements ProcessorInterface
+{
+ public function __construct(
+ private readonly PartInfoRetriever $infoRetriever,
+ private readonly ProviderRegistry $providerRegistry,
+ private readonly InfoProviderGeneralSettings $infoProviderSettings,
+ ) {
+ }
+
+ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
+ {
+ if (!$data instanceof InfoProviderSearchInput) {
+ throw new BadRequestHttpException('Expected InfoProviderSearchInput');
+ }
+
+ $providers = $this->resolveProviderKeys($data->providers);
+
+ try {
+ return $this->infoRetriever->searchByKeyword($data->keyword, $providers);
+ } catch (InfoProviderNotActiveException|\InvalidArgumentException $e) {
+ throw new BadRequestHttpException($e->getMessage(), $e);
+ }
+ }
+
+ /**
+ * Resolves the provider keys to search in. If none are given, falls back to the active default search
+ * providers configured in the system settings (mirroring the behavior of the info providers search page).
+ * @param string[]|null $providerKeys
+ * @return string[]
+ */
+ private function resolveProviderKeys(?array $providerKeys): array
+ {
+ if (!empty($providerKeys)) {
+ return $providerKeys;
+ }
+
+ $providers = [];
+ foreach ($this->infoProviderSettings->defaultSearchProviders as $providerKey) {
+ try {
+ if ($this->providerRegistry->getProviderByKey($providerKey)->isActive()) {
+ $providers[] = $providerKey;
+ }
+ } catch (\InvalidArgumentException) {
+ //Ignore providers configured as default, which do not exist (anymore)
+ }
+ }
+
+ if ($providers === []) {
+ throw new BadRequestHttpException(sprintf(
+ 'No providers were given and no default search providers are configured or active. Available provider keys: %s',
+ implode(', ', array_keys($this->providerRegistry->getActiveProviders()))
+ ));
+ }
+
+ return $providers;
+ }
+}
diff --git a/src/State/Mcp/SearchPartsProcessor.php b/src/State/Mcp/SearchPartsProcessor.php
new file mode 100644
index 00000000..8d922394
--- /dev/null
+++ b/src/State/Mcp/SearchPartsProcessor.php
@@ -0,0 +1,80 @@
+.
+ */
+
+declare(strict_types=1);
+
+
+namespace App\State\Mcp;
+
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProcessorInterface;
+use App\DataTables\Filters\PartSearchFilter;
+use App\Entity\Parts\Part;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\QueryBuilder;
+
+class SearchPartsProcessor implements ProcessorInterface
+{
+
+ public function __construct(
+ private readonly EntityManagerInterface $entityManager,
+ ) {
+
+ }
+
+ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
+ {
+ if (!$data instanceof PartSearchFilter) {
+ return [];
+ }
+
+ $qb = $this->entityManager->getRepository(Part::class)->createQueryBuilder('part');
+
+ $data->apply($qb);
+ $this->addJoins($qb);
+
+ $qb->addGroupBy('part');
+
+ return $qb->getQuery()->getResult();
+ }
+
+ private function addJoins(QueryBuilder $qb): void
+ {
+ $dql = $qb->getDQL();
+
+ if (str_contains($dql, '_category')) {
+ $qb->leftJoin('part.category', '_category');
+ }
+ if (str_contains($dql, '_storelocations')) {
+ $qb->leftJoin('part.partLots', '_partLots');
+ $qb->leftJoin('_partLots.storage_location', '_storelocations');
+ }
+ if (str_contains($dql, '_orderdetails') || str_contains($dql, '_suppliers')) {
+ $qb->leftJoin('part.orderdetails', '_orderdetails');
+ $qb->leftJoin('_orderdetails.supplier', '_suppliers');
+ }
+ if (str_contains($dql, '_manufacturer')) {
+ $qb->leftJoin('part.manufacturer', '_manufacturer');
+ }
+ if (str_contains($dql, '_footprint')) {
+ $qb->leftJoin('part.footprint', '_footprint');
+ }
+ }
+}
diff --git a/src/Twig/InfoProviderExtension.php b/src/Twig/InfoProviderExtension.php
index 54dbf93a..4de13115 100644
--- a/src/Twig/InfoProviderExtension.php
+++ b/src/Twig/InfoProviderExtension.php
@@ -59,7 +59,7 @@ final readonly class InfoProviderExtension
public function getInfoProviderName(string $key): ?string
{
try {
- return $this->providerRegistry->getProviderByKey($key)->getProviderInfo()['name'];
+ return $this->providerRegistry->getProviderByKey($key)->getProviderInfo()->name;
} catch (\InvalidArgumentException) {
return null;
}
diff --git a/symfony.lock b/symfony.lock
index 94af6e6a..67d42f71 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -597,6 +597,9 @@
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
+ "symfony/mcp-bundle": {
+ "version": "v0.8.0"
+ },
"symfony/mime": {
"version": "v4.3.1"
},
diff --git a/templates/components/search.macro.html.twig b/templates/components/search.macro.html.twig
index 90c01876..d4b25590 100644
--- a/templates/components/search.macro.html.twig
+++ b/templates/components/search.macro.html.twig
@@ -1,11 +1,11 @@
-{% macro settings_drodown(show_label_instead_icon = true) %}
+{% macro settings_dropdown(show_label_instead_icon = true) %}
{% if show_label_instead_icon %}{% trans %}search.options.label{% endtrans %}{% else %} {% endif %}
-