From 071ffe322dc8a34ff59eaa4227da878375cee05d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 26 Jul 2026 13:55:27 +0200 Subject: [PATCH] Reworked info provider system to merge all provider metadata into a unified DTO --- src/Controller/BrowserPluginController.php | 2 +- src/Controller/InfoProviderController.php | 2 +- .../InfoProviderNotActiveException.php | 3 +- .../InfoProviderSystem/ProviderSelectType.php | 8 +-- .../CreateFromUrlHelper.php | 2 +- .../DTOs/BulkSearchFieldMappingDTO.php | 2 +- ...nfoProviderDTO.php => ProviderInfoDTO.php} | 38 +++++++++-- .../InfoProviderSystem/PartInfoRetriever.php | 2 +- .../InfoProviderSystem/ProviderRegistry.php | 2 +- .../Providers/AIWebProvider.php | 44 ++++++------- .../Providers/BuerklinProvider.php | 44 ++++++------- .../Providers/CanopyProvider.php | 41 ++++++------ .../Providers/ConradProvider.php | 48 ++++++-------- .../Providers/DigikeyProvider.php | 52 +++++++-------- .../Providers/Element14Provider.php | 40 +++++------- .../Providers/EmptyProvider.php | 32 ++++------ .../Providers/GenericWebProvider.php | 42 ++++++------ .../Providers/InfoProviderInterface.php | 29 +-------- .../Providers/LCSCProvider.php | 44 ++++++------- .../Providers/MouserProvider.php | 43 ++++++------- .../Providers/OEMSecretsProvider.php | 64 ++++++------------- .../Providers/OctopartProvider.php | 44 ++++++------- .../Providers/PollinProvider.php | 44 ++++++------- .../Providers/ProviderCapabilities.php | 16 ++--- .../Providers/ReicheltProvider.php | 45 ++++++------- .../Providers/TMEProvider.php | 46 ++++++------- .../Providers/TestProvider.php | 41 +++++------- src/State/Mcp/ListInfoProvidersProcessor.php | 18 ++---- src/Twig/InfoProviderExtension.php | 2 +- .../info_providers/providers.macro.html.twig | 18 +++--- .../settings/provider_settings.html.twig | 2 +- .../Endpoints/InfoProviderEndpointTest.php | 10 +++ .../DTOs/BulkSearchFieldMappingDTOTest.php | 3 +- .../ProviderRegistryTest.php | 3 +- .../Providers/LCSCProviderTest.php | 19 ++---- .../Providers/TMEProviderTest.php | 18 ++---- ...etInfoProviderPartDetailsProcessorTest.php | 5 +- .../Mcp/ListInfoProvidersProcessorTest.php | 24 ++++--- .../Mcp/SearchInfoProvidersProcessorTest.php | 3 +- 39 files changed, 412 insertions(+), 533 deletions(-) rename src/Services/InfoProviderSystem/DTOs/{InfoProviderDTO.php => ProviderInfoDTO.php} (55%) diff --git a/src/Controller/BrowserPluginController.php b/src/Controller/BrowserPluginController.php index 1bb95787..403b0dc1 100644 --- a/src/Controller/BrowserPluginController.php +++ b/src/Controller/BrowserPluginController.php @@ -73,7 +73,7 @@ class BrowserPluginController extends AbstractController if (isset($activeProviders[$key])) { $urlProviders[] = [ 'id' => $key, - 'label' => $activeProviders[$key]->getProviderInfo()['name'], + 'label' => $activeProviders[$key]->getProviderInfo()->name, ]; } } diff --git a/src/Controller/InfoProviderController.php b/src/Controller/InfoProviderController.php index 28c281d0..09251043 100644 --- a/src/Controller/InfoProviderController.php +++ b/src/Controller/InfoProviderController.php @@ -88,7 +88,7 @@ class InfoProviderController extends AbstractController $this->denyAccessUnlessGranted('@info_providers.create_parts'); $providerInstance = $this->providerRegistry->getProviderByKey($provider); - $settingsClass = $providerInstance->getProviderInfo()['settings_class'] ?? throw new \LogicException('Provider ' . $provider . ' does not have a settings class defined'); + $settingsClass = $providerInstance->getProviderInfo()->settingsClass ?? throw new \LogicException('Provider ' . $provider . ' does not have a settings class defined'); //Create a clone of the settings object $settings = $this->settingsManager->createTemporaryCopy($settingsClass); diff --git a/src/Exceptions/InfoProviderNotActiveException.php b/src/Exceptions/InfoProviderNotActiveException.php index 02f7cfb7..16e71af8 100644 --- a/src/Exceptions/InfoProviderNotActiveException.php +++ b/src/Exceptions/InfoProviderNotActiveException.php @@ -43,6 +43,7 @@ class InfoProviderNotActiveException extends \RuntimeException */ public static function fromProvider(InfoProviderInterface $provider): self { - return new self($provider->getProviderKey(), $provider->getProviderInfo()['name'] ?? '???'); + $info = $provider->getProviderInfo(); + return new self($info->key, $info->name); } } diff --git a/src/Form/InfoProviderSystem/ProviderSelectType.php b/src/Form/InfoProviderSystem/ProviderSelectType.php index e59dc85f..a29ebdf2 100644 --- a/src/Form/InfoProviderSystem/ProviderSelectType.php +++ b/src/Form/InfoProviderSystem/ProviderSelectType.php @@ -66,8 +66,8 @@ class ProviderSelectType extends AbstractType $tmp = []; foreach ($providers as $provider) { - $name = $provider->getProviderInfo()['name']; - $tmp[$name] = $provider->getProviderKey(); + $info = $provider->getProviderInfo(); + $tmp[$info->name] = $info->key; } return $tmp; @@ -77,7 +77,7 @@ class ProviderSelectType extends AbstractType $resolver->setDefault('choice_label', function (Options $options) { if ('object' === $options['input']) { return ChoiceList::label($this, static fn(?InfoProviderInterface $choice - ) => new StaticMessage($choice?->getProviderInfo()['name'])); + ) => new StaticMessage($choice?->getProviderInfo()->name)); } return static fn($choice, $key, $value) => new StaticMessage($key); @@ -85,7 +85,7 @@ class ProviderSelectType extends AbstractType $resolver->setDefault('choice_value', function (Options $options) { if ('object' === $options['input']) { return ChoiceList::value($this, - static fn(?InfoProviderInterface $choice) => $choice?->getProviderKey()); + static fn(?InfoProviderInterface $choice) => $choice?->getProviderInfo()->key); } return null; diff --git a/src/Services/InfoProviderSystem/CreateFromUrlHelper.php b/src/Services/InfoProviderSystem/CreateFromUrlHelper.php index 0291142f..d844cdaa 100644 --- a/src/Services/InfoProviderSystem/CreateFromUrlHelper.php +++ b/src/Services/InfoProviderSystem/CreateFromUrlHelper.php @@ -72,7 +72,7 @@ final readonly class CreateFromUrlHelper $provider = $this->providerRegistry->getProviderHandlingDomain($host); - if ($provider !== null && $provider->isActive() && $provider->getProviderKey() !== $callingInfoProvider->getProviderKey()) { + if ($provider !== null && $provider->isActive() && $provider->getProviderInfo()->key !== $callingInfoProvider->getProviderInfo()->key) { try { $id = $provider->getIDFromURL($url); if ($id !== null) { diff --git a/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php b/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php index 47d8ac69..e13d3b82 100644 --- a/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php @@ -49,7 +49,7 @@ readonly class BulkSearchFieldMappingDTO //Ensure that providers are provided as keys foreach ($providers as &$provider) { if ($provider instanceof InfoProviderInterface) { - $provider = $provider->getProviderKey(); + $provider = $provider->getProviderInfo()->key; } if (!is_string($provider)) { throw new \InvalidArgumentException('Providers must be provided as strings or InfoProviderInterface instances'); diff --git a/src/Services/InfoProviderSystem/DTOs/InfoProviderDTO.php b/src/Services/InfoProviderSystem/DTOs/ProviderInfoDTO.php similarity index 55% rename from src/Services/InfoProviderSystem/DTOs/InfoProviderDTO.php rename to src/Services/InfoProviderSystem/DTOs/ProviderInfoDTO.php index 5773bc49..1e51e5c6 100644 --- a/src/Services/InfoProviderSystem/DTOs/InfoProviderDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/ProviderInfoDTO.php @@ -27,11 +27,17 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\McpToolCollection; use ApiPlatform\OpenApi\Model\Operation; use App\Mcp\DTO\ListInfoProvidersInput; +use App\Services\InfoProviderSystem\Providers\ProviderCapabilities; use App\State\Mcp\ListInfoProvidersProcessor; +use Symfony\Component\Serializer\Annotation\Groups; /** - * This DTO represents an available info provider (a distributor or manufacturer catalog which can be - * searched via search_info_providers / get_info_provider_part_details). + * Immutable, structured description of an info provider, returned by InfoProviderInterface::getProviderInfo() + * and (via the 'info_provider:read' group) exposed as the REST GET /api/info_providers collection and the + * list_info_providers MCP tool. + * + * disabledHelp, oauthAppName and settingsClass are internal-only (used by the settings UI) and deliberately + * not tagged with the 'info_provider:read' group, so they never appear in the API/MCP output. */ #[ApiResource( uriTemplate: '/info_providers', @@ -44,6 +50,7 @@ use App\State\Mcp\ListInfoProvidersProcessor; ), ], paginationEnabled: false, + normalizationContext: ['groups' => ['info_provider:read']], mcp: [ 'list_info_providers' => new McpToolCollection( title: 'List available info providers', @@ -52,21 +59,38 @@ use App\State\Mcp\ListInfoProvidersProcessor; input: ListInfoProvidersInput::class, security: 'is_granted("@info_providers.create_parts")', processor: ListInfoProvidersProcessor::class, + normalizationContext: ['groups' => ['info_provider:read']], ), ], )] -readonly class InfoProviderDTO +readonly class ProviderInfoDTO { public function __construct( - /** @var string The unique key of the provider, to be used as provider_key in the other info provider tools */ + /** @var string A unique key for this provider (e.g. "digikey"), which is saved into the database and used to identify the provider */ + #[Groups(['info_provider:read'])] public string $key, - /** @var string The (user friendly) name of the provider (e.g. "Digikey") */ + /** @var string The (user friendly) name of the provider (e.g. "Digikey"), will be translated */ + #[Groups(['info_provider:read'])] public string $name, - /** @var string|null A short description of the provider */ + /** @var string|null A short description of the provider (e.g. "Digikey is a ..."), will be translated */ + #[Groups(['info_provider:read'])] public ?string $description = null, /** @var string|null The url of the provider (e.g. "https://www.digikey.com") */ + #[Groups(['info_provider:read'])] public ?string $url = null, - /** @var string[] The kinds of data this provider can supply (e.g. "PRICE", "DATASHEET", "PICTURE") */ + /** @var string|null A help text which is shown when the provider is disabled, explaining how to enable it */ + public ?string $disabledHelp = null, + /** @var string|null 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 */ + public ?string $oauthAppName = null, + /** @var class-string|null 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 */ + public ?string $settingsClass = null, + /** + * 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. + * @var ProviderCapabilities[] + */ + #[Groups(['info_provider:read'])] public array $capabilities = [], ) { } diff --git a/src/Services/InfoProviderSystem/PartInfoRetriever.php b/src/Services/InfoProviderSystem/PartInfoRetriever.php index 9cff3312..f99970af 100644 --- a/src/Services/InfoProviderSystem/PartInfoRetriever.php +++ b/src/Services/InfoProviderSystem/PartInfoRetriever.php @@ -103,7 +103,7 @@ final class PartInfoRetriever //Generate a hash for the options, to ensure that different options result in different cache entries $options_hash = hash('xxh3', json_encode($options_without_cache, JSON_THROW_ON_ERROR)); - $cache_key = "search_{$provider->getProviderKey()}_{$escaped_keyword}_{$options_hash}"; + $cache_key = "search_{$provider->getProviderInfo()->key}_{$escaped_keyword}_{$options_hash}"; //If no_cache is set, bypass the cache and get fresh results from the provider if ($no_cache) { 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..7f977036 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,22 @@ 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, + ], + ); } public function isActive(): bool @@ -166,7 +171,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 +261,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..fe110971 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,23 @@ 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, + ], + ); } // This provider is considered active if settings are present @@ -283,7 +288,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 +514,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..2f0f355a 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,21 @@ 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, + ], + ); } public function isActive(): bool @@ -131,7 +134,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 +212,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 +225,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..9af1513f 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,24 @@ 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, + ], + ); } public function isActive(): bool @@ -111,7 +117,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 +299,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 +317,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..4bd29a5c 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,24 @@ 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, + ], + ); } public function isActive(): bool @@ -128,7 +122,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 +135,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 +162,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 +185,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..179c2fb6 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,21 @@ 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, + ], + ); } public function isActive(): bool @@ -113,7 +116,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 +304,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..ab96ec0d 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,30 @@ 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, + ], + ); } // This provider is always active @@ -227,7 +232,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 +460,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..4d3780da 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,22 @@ 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, + ], + ); } /** @@ -456,17 +443,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 +872,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 +892,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 +1246,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..b26df2bb 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,23 @@ 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, + ], + ); } public function isActive(): bool @@ -307,7 +313,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 +403,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..bf7ac732 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,22 @@ 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, + ], + ); } public function isActive(): bool @@ -88,7 +92,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 +160,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 +248,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..b92a8e4c 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,23 @@ 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, + ], + ); } public function isActive(): bool @@ -93,7 +98,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 +178,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 +287,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..0ffc8bb7 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,23 @@ 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, + ], + ); } public function isActive(): bool @@ -83,7 +88,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 +124,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 +295,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/State/Mcp/ListInfoProvidersProcessor.php b/src/State/Mcp/ListInfoProvidersProcessor.php index 774ad4ce..6b995a15 100644 --- a/src/State/Mcp/ListInfoProvidersProcessor.php +++ b/src/State/Mcp/ListInfoProvidersProcessor.php @@ -25,7 +25,7 @@ namespace App\State\Mcp; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProviderInterface; -use App\Services\InfoProviderSystem\DTOs\InfoProviderDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\ProviderRegistry; /** @@ -40,7 +40,7 @@ class ListInfoProvidersProcessor implements ProcessorInterface, ProviderInterfac } /** - * @return InfoProviderDTO[] + * @return ProviderInfoDTO[] */ public function provide(Operation $operation, array $uriVariables = [], array $context = []): array { @@ -48,7 +48,7 @@ class ListInfoProvidersProcessor implements ProcessorInterface, ProviderInterfac } /** - * @return InfoProviderDTO[] + * @return ProviderInfoDTO[] */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): array { @@ -56,22 +56,14 @@ class ListInfoProvidersProcessor implements ProcessorInterface, ProviderInterfac } /** - * @return InfoProviderDTO[] + * @return ProviderInfoDTO[] */ private function listActiveProviders(): array { $result = []; foreach ($this->providerRegistry->getActiveProviders() as $provider) { - $info = $provider->getProviderInfo(); - - $result[] = new InfoProviderDTO( - key: $provider->getProviderKey(), - name: $info['name'], - description: $info['description'] ?? null, - url: $info['url'] ?? null, - capabilities: array_map(static fn ($capability) => $capability->name, $provider->getCapabilities()), - ); + $result[] = $provider->getProviderInfo(); } return $result; 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/templates/info_providers/providers.macro.html.twig b/templates/info_providers/providers.macro.html.twig index bec8f24b..62a32502 100644 --- a/templates/info_providers/providers.macro.html.twig +++ b/templates/info_providers/providers.macro.html.twig @@ -8,35 +8,35 @@
- {% if provider.providerInfo.url is defined and provider.providerInfo.url is not empty %} + {% if provider.providerInfo.url is not empty %} {{ provider.providerInfo.name }} {% else %} {{ provider.providerInfo.name | trans }} {% endif %}
- {% if provider.providerInfo.description is defined and provider.providerInfo.description is not null %} + {% if provider.providerInfo.description is not null %} {{ provider.providerInfo.description | trans }} {% endif %}
- {% if provider.providerInfo.settings_class is defined %} - {% endif %} - {% for capability in provider.capabilities|sort((a, b) => a.orderIndex <=> b.orderIndex) %} + {% for capability in provider.providerInfo.capabilities|sort((a, b) => a.orderIndex <=> b.orderIndex) %} {# @var capability \App\Services\InfoProviderSystem\Providers\ProviderCapabilities #} {{ capability.translationKey|trans }} {% endfor %} - {% if provider.providerInfo.oauth_app_name is defined and provider.providerInfo.oauth_app_name is not empty %} + {% if provider.providerInfo.oauthAppName is not empty %}
- {% trans %}oauth_client.connect.btn{% endtrans %} + {% trans %}oauth_client.connect.btn{% endtrans %} {% endif %}
@@ -44,9 +44,9 @@
{% trans %}info_providers.providers_list.disabled{% endtrans %} - {% if provider.providerInfo.disabled_help is defined and provider.providerInfo.disabled_help is not empty %} + {% if provider.providerInfo.disabledHelp is not empty %}
- {{ provider.providerInfo.disabled_help|trans }} + {{ provider.providerInfo.disabledHelp|trans }} {% endif %}
diff --git a/templates/info_providers/settings/provider_settings.html.twig b/templates/info_providers/settings/provider_settings.html.twig index db942f8a..1159fbf3 100644 --- a/templates/info_providers/settings/provider_settings.html.twig +++ b/templates/info_providers/settings/provider_settings.html.twig @@ -10,7 +10,7 @@ {% block card_content %}

- {% if info_provider_info.url is defined %} + {% if info_provider_info.url is not empty %} {{ info_provider_info.name }} {% else %} {{ info_provider_info.name }} diff --git a/tests/API/Endpoints/InfoProviderEndpointTest.php b/tests/API/Endpoints/InfoProviderEndpointTest.php index 7203b15c..fc89ad0d 100644 --- a/tests/API/Endpoints/InfoProviderEndpointTest.php +++ b/tests/API/Endpoints/InfoProviderEndpointTest.php @@ -39,6 +39,16 @@ class InfoProviderEndpointTest extends AuthenticatedApiTestCase $keys = array_column($json['hydra:member'], 'key'); self::assertContains('test', $keys); + + //The 'test' provider has a disabledHelp set internally, but it must not leak into the API response + $testProvider = $json['hydra:member'][array_search('test', $keys, true)]; + self::assertArrayHasKey('capabilities', $testProvider); + self::assertArrayNotHasKey('disabledHelp', $testProvider); + self::assertArrayNotHasKey('oauthAppName', $testProvider); + self::assertArrayNotHasKey('settingsClass', $testProvider); + + //Capabilities must serialize as plain enum-name strings, not objects + self::assertSame(['BASIC', 'FOOTPRINT'], $testProvider['capabilities']); } public function testListInfoProvidersRequiresAuthentication(): void diff --git a/tests/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTOTest.php b/tests/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTOTest.php index 2fd50f9a..a8d87dc5 100644 --- a/tests/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTOTest.php +++ b/tests/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTOTest.php @@ -24,6 +24,7 @@ namespace App\Tests\Services\InfoProviderSystem\DTOs; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use PHPUnit\Framework\TestCase; final class BulkSearchFieldMappingDTOTest extends TestCase @@ -32,7 +33,7 @@ final class BulkSearchFieldMappingDTOTest extends TestCase public function testProviderInstanceNormalization(): void { $mockProvider = $this->createMock(InfoProviderInterface::class); - $mockProvider->method('getProviderKey')->willReturn('mock_provider'); + $mockProvider->method('getProviderInfo')->willReturn(new ProviderInfoDTO(key: 'mock_provider', name: 'mock_provider')); $fieldMapping = new BulkSearchFieldMappingDTO(field: 'mpn', providers: ['provider1', $mockProvider], priority: 5); $this->assertSame(['provider1', 'mock_provider'], $fieldMapping->providers); diff --git a/tests/Services/InfoProviderSystem/ProviderRegistryTest.php b/tests/Services/InfoProviderSystem/ProviderRegistryTest.php index d3fce441..3043edff 100644 --- a/tests/Services/InfoProviderSystem/ProviderRegistryTest.php +++ b/tests/Services/InfoProviderSystem/ProviderRegistryTest.php @@ -22,6 +22,7 @@ declare(strict_types=1); */ namespace App\Tests\Services\InfoProviderSystem; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\ProviderRegistry; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use App\Services\InfoProviderSystem\Providers\URLHandlerInfoProviderInterface; @@ -46,7 +47,7 @@ final class ProviderRegistryTest extends TestCase public function getMockProvider(string $key, bool $active = true): InfoProviderInterface { $mock = $this->createMockForIntersectionOfInterfaces([InfoProviderInterface::class, URLHandlerInfoProviderInterface::class]); - $mock->method('getProviderKey')->willReturn($key); + $mock->method('getProviderInfo')->willReturn(new ProviderInfoDTO(key: $key, name: $key)); $mock->method('isActive')->willReturn($active); $mock->method('getHandledDomains')->willReturn(["$key.com", "test.$key.de"]); diff --git a/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php b/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php index 63e1b452..bce0a2cf 100644 --- a/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php +++ b/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.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\Providers\LCSCProvider; use App\Services\InfoProviderSystem\Providers\ProviderCapabilities; @@ -56,18 +57,10 @@ final class LCSCProviderTest extends TestCase { $info = $this->provider->getProviderInfo(); - $this->assertIsArray($info); - $this->assertArrayHasKey('name', $info); - $this->assertArrayHasKey('description', $info); - $this->assertArrayHasKey('url', $info); - $this->assertArrayHasKey('disabled_help', $info); - $this->assertEquals('LCSC', $info['name']); - $this->assertEquals('https://www.lcsc.com/', $info['url']); - } - - public function testGetProviderKey(): void - { - $this->assertSame('lcsc', $this->provider->getProviderKey()); + $this->assertInstanceOf(ProviderInfoDTO::class, $info); + $this->assertSame('lcsc', $info->key); + $this->assertEquals('LCSC', $info->name); + $this->assertEquals('https://www.lcsc.com/', $info->url); } public function testIsActiveWhenEnabled(): void @@ -86,7 +79,7 @@ final class LCSCProviderTest extends TestCase public function testGetCapabilities(): void { - $capabilities = $this->provider->getCapabilities(); + $capabilities = $this->provider->getProviderInfo()->capabilities; $this->assertIsArray($capabilities); $this->assertContains(ProviderCapabilities::BASIC, $capabilities); diff --git a/tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php b/tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php index 20fba523..be4629e5 100644 --- a/tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php +++ b/tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php @@ -24,6 +24,7 @@ namespace App\Tests\Services\InfoProviderSystem\Providers; use App\Entity\Parts\ManufacturingStatus; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\Providers\ProviderCapabilities; @@ -212,17 +213,10 @@ final class TMEProviderTest extends TestCase { $info = $this->provider->getProviderInfo(); - $this->assertIsArray($info); - $this->assertArrayHasKey('name', $info); - $this->assertArrayHasKey('description', $info); - $this->assertArrayHasKey('url', $info); - $this->assertEquals('TME', $info['name']); - $this->assertEquals('https://tme.eu/', $info['url']); - } - - public function testGetProviderKey(): void - { - $this->assertSame('tme', $this->provider->getProviderKey()); + $this->assertInstanceOf(ProviderInfoDTO::class, $info); + $this->assertSame('tme', $info->key); + $this->assertEquals('TME', $info->name); + $this->assertEquals('https://tme.eu/', $info->url); } public function testIsActiveWithCredentials(): void @@ -239,7 +233,7 @@ final class TMEProviderTest extends TestCase public function testGetCapabilities(): void { - $capabilities = $this->provider->getCapabilities(); + $capabilities = $this->provider->getProviderInfo()->capabilities; $this->assertIsArray($capabilities); $this->assertContains(ProviderCapabilities::BASIC, $capabilities); diff --git a/tests/State/Mcp/GetInfoProviderPartDetailsProcessorTest.php b/tests/State/Mcp/GetInfoProviderPartDetailsProcessorTest.php index 07fcdc27..2f6e61ba 100644 --- a/tests/State/Mcp/GetInfoProviderPartDetailsProcessorTest.php +++ b/tests/State/Mcp/GetInfoProviderPartDetailsProcessorTest.php @@ -26,6 +26,7 @@ namespace App\Tests\State\Mcp; use ApiPlatform\Metadata\Get; use App\Mcp\DTO\InfoProviderPartDetailsInput; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOtoEntityConverter; use App\Services\InfoProviderSystem\PartInfoRetriever; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; @@ -45,14 +46,14 @@ final class GetInfoProviderPartDetailsProcessorTest extends TestCase protected function setUp(): void { $activeProvider = $this->createMock(InfoProviderInterface::class); - $activeProvider->method('getProviderKey')->willReturn('test1'); + $activeProvider->method('getProviderInfo')->willReturn(new ProviderInfoDTO(key: 'test1', name: 'test1')); $activeProvider->method('isActive')->willReturn(true); $activeProvider->method('getDetails')->willReturn( new PartDetailDTO(provider_key: 'test1', provider_id: '42', name: 'Element 42', description: 'desc') ); $inactiveProvider = $this->createMock(InfoProviderInterface::class); - $inactiveProvider->method('getProviderKey')->willReturn('test2'); + $inactiveProvider->method('getProviderInfo')->willReturn(new ProviderInfoDTO(key: 'test2', name: 'test2')); $inactiveProvider->method('isActive')->willReturn(false); $providerRegistry = new ProviderRegistry([$activeProvider, $inactiveProvider]); diff --git a/tests/State/Mcp/ListInfoProvidersProcessorTest.php b/tests/State/Mcp/ListInfoProvidersProcessorTest.php index a255aee3..b37e85f4 100644 --- a/tests/State/Mcp/ListInfoProvidersProcessorTest.php +++ b/tests/State/Mcp/ListInfoProvidersProcessorTest.php @@ -25,7 +25,7 @@ namespace App\Tests\State\Mcp; use ApiPlatform\Metadata\Get; use App\Mcp\DTO\ListInfoProvidersInput; -use App\Services\InfoProviderSystem\DTOs\InfoProviderDTO; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use App\Services\InfoProviderSystem\Providers\ProviderCapabilities; use App\Services\InfoProviderSystem\ProviderRegistry; @@ -37,20 +37,18 @@ final class ListInfoProvidersProcessorTest extends TestCase public function testOnlyActiveProvidersAreListed(): void { $active = $this->createMock(InfoProviderInterface::class); - $active->method('getProviderKey')->willReturn('active_provider'); $active->method('isActive')->willReturn(true); - $active->method('getProviderInfo')->willReturn([ - 'name' => 'Active Provider', - 'description' => 'A provider that is active', - 'url' => 'https://example.com', - ]); - $active->method('getCapabilities')->willReturn([ProviderCapabilities::BASIC, ProviderCapabilities::PRICE]); + $active->method('getProviderInfo')->willReturn(new ProviderInfoDTO( + key: 'active_provider', + name: 'Active Provider', + description: 'A provider that is active', + url: 'https://example.com', + capabilities: [ProviderCapabilities::BASIC, ProviderCapabilities::PRICE], + )); $disabled = $this->createMock(InfoProviderInterface::class); - $disabled->method('getProviderKey')->willReturn('disabled_provider'); $disabled->method('isActive')->willReturn(false); - $disabled->method('getProviderInfo')->willReturn(['name' => 'Disabled Provider']); - $disabled->method('getCapabilities')->willReturn([]); + $disabled->method('getProviderInfo')->willReturn(new ProviderInfoDTO(key: 'disabled_provider', name: 'Disabled Provider')); $registry = new ProviderRegistry([$active, $disabled]); $processor = new ListInfoProvidersProcessor($registry); @@ -58,11 +56,11 @@ final class ListInfoProvidersProcessorTest extends TestCase $result = $processor->process(new ListInfoProvidersInput(), new Get()); $this->assertCount(1, $result); - $this->assertInstanceOf(InfoProviderDTO::class, $result[0]); + $this->assertInstanceOf(ProviderInfoDTO::class, $result[0]); $this->assertSame('active_provider', $result[0]->key); $this->assertSame('Active Provider', $result[0]->name); $this->assertSame('A provider that is active', $result[0]->description); $this->assertSame('https://example.com', $result[0]->url); - $this->assertSame(['BASIC', 'PRICE'], $result[0]->capabilities); + $this->assertSame([ProviderCapabilities::BASIC, ProviderCapabilities::PRICE], $result[0]->capabilities); } } diff --git a/tests/State/Mcp/SearchInfoProvidersProcessorTest.php b/tests/State/Mcp/SearchInfoProvidersProcessorTest.php index 2a56a9a2..52c24832 100644 --- a/tests/State/Mcp/SearchInfoProvidersProcessorTest.php +++ b/tests/State/Mcp/SearchInfoProvidersProcessorTest.php @@ -25,6 +25,7 @@ namespace App\Tests\State\Mcp; use ApiPlatform\Metadata\Get; use App\Mcp\DTO\InfoProviderSearchInput; +use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOtoEntityConverter; use App\Services\InfoProviderSystem\PartInfoRetriever; @@ -69,8 +70,8 @@ final class SearchInfoProvidersProcessorTest extends TestCase private function getMockProvider(string $key, bool $active = true): InfoProviderInterface { $mock = $this->createMock(InfoProviderInterface::class); - $mock->method('getProviderKey')->willReturn($key); $mock->method('isActive')->willReturn($active); + $mock->method('getProviderInfo')->willReturn(new ProviderInfoDTO(key: $key, name: $key)); $mock->method('searchByKeyword')->willReturn([ new SearchResultDTO(provider_key: $key, provider_id: '1', name: 'Element 1', description: 'desc'), ]);