diff --git a/src/Controller/BrowserPluginController.php b/src/Controller/BrowserPluginController.php index 403b0dc1..1bb95787 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 09251043..28c281d0 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()->settingsClass ?? throw new \LogicException('Provider ' . $provider . ' does not have a settings class defined'); + $settingsClass = $providerInstance->getProviderInfo()['settings_class'] ?? 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 16e71af8..02f7cfb7 100644 --- a/src/Exceptions/InfoProviderNotActiveException.php +++ b/src/Exceptions/InfoProviderNotActiveException.php @@ -43,7 +43,6 @@ class InfoProviderNotActiveException extends \RuntimeException */ public static function fromProvider(InfoProviderInterface $provider): self { - $info = $provider->getProviderInfo(); - return new self($info->key, $info->name); + return new self($provider->getProviderKey(), $provider->getProviderInfo()['name'] ?? '???'); } } diff --git a/src/Form/InfoProviderSystem/ProviderSelectType.php b/src/Form/InfoProviderSystem/ProviderSelectType.php index a29ebdf2..e59dc85f 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) { - $info = $provider->getProviderInfo(); - $tmp[$info->name] = $info->key; + $name = $provider->getProviderInfo()['name']; + $tmp[$name] = $provider->getProviderKey(); } 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?->getProviderInfo()->key); + static fn(?InfoProviderInterface $choice) => $choice?->getProviderKey()); } return null; diff --git a/src/Mcp/JsonSchema/FixedSchemaFactory.php b/src/Mcp/JsonSchema/FixedSchemaFactory.php new file mode 100644 index 00000000..c851c394 --- /dev/null +++ b/src/Mcp/JsonSchema/FixedSchemaFactory.php @@ -0,0 +1,162 @@ +. + */ + +declare(strict_types=1); + +namespace App\Mcp\JsonSchema; + +use ApiPlatform\JsonSchema\Schema; +use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\Metadata\Operation; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; + +/** + * Overwrite the default JSON Schema factory to resolve $ref and allOf into a flat schema. + * This is a workaround until https://github.com/api-platform/core/pull/7962 is merged + */ +#[AsAlias('api_platform.mcp.json_schema.schema_factory')] +readonly class FixedSchemaFactory implements SchemaFactoryInterface +{ + public function __construct( + private readonly SchemaFactoryInterface $decorated, + ) { + } + + public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema + { + $schema = $this->decorated->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); + + $definitions = []; + foreach ($schema->getDefinitions() as $key => $definition) { + $definitions[$key] = $definition instanceof \ArrayObject ? $definition->getArrayCopy() : (array) $definition; + } + + $rootKey = $schema->getRootDefinitionKey(); + if (null !== $rootKey) { + $root = $definitions[$rootKey] ?? []; + } else { + // Collection schemas (and others) put allOf/type directly on the root + $root = $schema->getArrayCopy(false); + } + + $flat = self::resolveNode($root, $definitions); + + $flatSchema = new Schema(Schema::VERSION_JSON_SCHEMA); + unset($flatSchema['$schema']); + foreach ($flat as $key => $value) { + $flatSchema[$key] = $value; + } + + return $flatSchema; + } + + /** + * Recursively resolve $ref, allOf, and nested structures into a flat schema node. + * + * @param array $resolving Tracks the current $ref resolution chain to detect circular references + */ + public static function resolveNode(array|\ArrayObject $node, array $definitions, array &$resolving = []): array + { + if ($node instanceof \ArrayObject) { + $node = $node->getArrayCopy(); + } + + if (isset($node['$ref'])) { + $refKey = str_replace('#/definitions/', '', $node['$ref']); + if (!isset($definitions[$refKey]) || isset($resolving[$refKey])) { + return ['type' => 'object']; + } + $resolving[$refKey] = true; + $resolved = self::resolveNode($definitions[$refKey], $definitions, $resolving); + unset($resolving[$refKey]); + + return $resolved; + } + + if (isset($node['allOf'])) { + $merged = ['type' => 'object', 'properties' => []]; + $requiredSets = []; + foreach ($node['allOf'] as $entry) { + $resolved = self::resolveNode($entry, $definitions, $resolving); + if (isset($resolved['properties'])) { + foreach ($resolved['properties'] as $k => $v) { + $merged['properties'][$k] = $v; + } + } + if (isset($resolved['required'])) { + $requiredSets[] = $resolved['required']; + } + } + + if ($requiredSets) { + $merged['required'] = array_merge(...$requiredSets); + } + if ([] === $merged['properties']) { + unset($merged['properties']); + } + if (isset($node['description'])) { + $merged['description'] = $node['description']; + } + + return self::resolveDeep($merged, $definitions, $resolving); + } + + // oneOf/anyOf nodes must not receive a type fallback — their type is expressed + // through the sub-schemas. Adding 'type: object' here would break schemas like + // HydraItemBaseSchema's @context, which is oneOf: [string, object]. + if (isset($node['oneOf']) || isset($node['anyOf'])) { + return self::resolveDeep($node, $definitions, $resolving); + } + + if (!isset($node['type'])) { + $node['type'] = 'object'; + } + + return self::resolveDeep($node, $definitions, $resolving); + } + + /** + * Recursively resolve nested properties and array items. + */ + private static function resolveDeep(array $node, array $definitions, array &$resolving): array + { + if (isset($node['items'])) { + $node['items'] = self::resolveNode( + $node['items'] instanceof \ArrayObject ? $node['items']->getArrayCopy() : $node['items'], + $definitions, + $resolving, + ); + } + + if (isset($node['properties']) && \is_array($node['properties'])) { + foreach ($node['properties'] as $propName => $propSchema) { + $node['properties'][$propName] = self::resolveNode( + $propSchema instanceof \ArrayObject ? $propSchema->getArrayCopy() : $propSchema, + $definitions, + $resolving, + ); + } + } + + return $node; + } +} diff --git a/src/Services/InfoProviderSystem/CreateFromUrlHelper.php b/src/Services/InfoProviderSystem/CreateFromUrlHelper.php index d844cdaa..0291142f 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->getProviderInfo()->key !== $callingInfoProvider->getProviderInfo()->key) { + if ($provider !== null && $provider->isActive() && $provider->getProviderKey() !== $callingInfoProvider->getProviderKey()) { 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 e13d3b82..47d8ac69 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->getProviderInfo()->key; + $provider = $provider->getProviderKey(); } 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/InfoProviderDTO.php new file mode 100644 index 00000000..5773bc49 --- /dev/null +++ b/src/Services/InfoProviderSystem/DTOs/InfoProviderDTO.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\InfoProviderSystem\DTOs; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\McpToolCollection; +use ApiPlatform\OpenApi\Model\Operation; +use App\Mcp\DTO\ListInfoProvidersInput; +use App\State\Mcp\ListInfoProvidersProcessor; + +/** + * 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). + */ +#[ApiResource( + uriTemplate: '/info_providers', + description: 'An info provider which can be used to search for parts and retrieve part details.', + operations: [ + new GetCollection( + security: 'is_granted("@info_providers.create_parts")', + provider: ListInfoProvidersProcessor::class, + openapi: new Operation(summary: 'List the info providers which are currently active and can be used for searching parts.'), + ), + ], + paginationEnabled: false, + mcp: [ + 'list_info_providers' => new McpToolCollection( + title: 'List available info providers', + description: 'List the info providers (e.g. distributors like Digikey, Mouser, LCSC) which are currently active and can be used with search_info_providers and get_info_provider_part_details.', + annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], + input: ListInfoProvidersInput::class, + security: 'is_granted("@info_providers.create_parts")', + processor: ListInfoProvidersProcessor::class, + ), + ], +)] +readonly class InfoProviderDTO +{ + public function __construct( + /** @var string The unique key of the provider, to be used as provider_key in the other info provider tools */ + public string $key, + /** @var string The (user friendly) name of the provider (e.g. "Digikey") */ + public string $name, + /** @var string|null A short description of the provider */ + public ?string $description = null, + /** @var string|null The url of the provider (e.g. "https://www.digikey.com") */ + public ?string $url = null, + /** @var string[] The kinds of data this provider can supply (e.g. "PRICE", "DATASHEET", "PICTURE") */ + public array $capabilities = [], + ) { + } +} diff --git a/src/Services/InfoProviderSystem/DTOs/ProviderInfoDTO.php b/src/Services/InfoProviderSystem/DTOs/ProviderInfoDTO.php deleted file mode 100644 index 248bf997..00000000 --- a/src/Services/InfoProviderSystem/DTOs/ProviderInfoDTO.php +++ /dev/null @@ -1,104 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace App\Services\InfoProviderSystem\DTOs; - -use ApiPlatform\Metadata\ApiResource; -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; - -/** - * 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', - description: 'An info provider which can be used to search for parts and retrieve part details.', - operations: [ - new GetCollection( - openapi: new Operation(summary: 'List the info providers which are currently active and can be used for searching parts.'), - security: 'is_granted("@info_providers.create_parts")', - provider: ListInfoProvidersProcessor::class, - ), - ], - normalizationContext: ['groups' => ['info_provider:read']], - paginationEnabled: false, - mcp: [ - 'list_info_providers' => new McpToolCollection( - title: 'List available info providers', - description: 'List the info providers (e.g. distributors like Digikey, Mouser, LCSC) which are currently active and can be used with search_info_providers and get_info_provider_part_details. Ask for the user\'s confirmation before using expensive providers (e.g. distributors with strict rate limits or which cost money to use).', - annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false], - normalizationContext: ['groups' => ['info_provider:read']], - security: 'is_granted("@info_providers.create_parts")', - input: ListInfoProvidersInput::class, - processor: ListInfoProvidersProcessor::class, - ), - ], -)] -readonly class ProviderInfoDTO -{ - public function __construct( - /** @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"), will be translated */ - #[Groups(['info_provider:read'])] - public string $name, - /** @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|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 = [], - /** - * @var bool True if this provider is considered "expensive" (e.g. it has a strict rate limit or costs money to use), false otherwise. - * Users should be asked for confirmation before using an expensive provider, when making multiple requests (e.g. when searching for multiple parts at once), - * and more careful caching should be used for expensive providers. - */ - #[Groups(['info_provider:read'])] - public bool $expensive = false, - ) { - } -} diff --git a/src/Services/InfoProviderSystem/PartInfoRetriever.php b/src/Services/InfoProviderSystem/PartInfoRetriever.php index f99970af..9cff3312 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->getProviderInfo()->key}_{$escaped_keyword}_{$options_hash}"; + $cache_key = "search_{$provider->getProviderKey()}_{$escaped_keyword}_{$options_hash}"; //If no_cache is set, bypass the cache and get fresh results from the provider if ($no_cache) { diff --git a/src/Services/InfoProviderSystem/ProviderRegistry.php b/src/Services/InfoProviderSystem/ProviderRegistry.php index 1b0b9785..18b8a37a 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->getProviderInfo()->key; + $key = $provider->getProviderKey(); 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 feb94384..be91041e 100644 --- a/src/Services/InfoProviderSystem/Providers/AIWebProvider.php +++ b/src/Services/InfoProviderSystem/Providers/AIWebProvider.php @@ -31,7 +31,6 @@ 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; @@ -51,8 +50,6 @@ final class AIWebProvider implements InfoProviderInterface { use FixAndValidateUrlTrait; - public const PROVIDER_KEY = 'ai_web'; - private const DISTRIBUTOR_NAME = 'Website'; private readonly HttpClientInterface $httpClient; @@ -74,23 +71,20 @@ final class AIWebProvider implements InfoProviderInterface ); } - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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, - ); + 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'; } public function isActive(): bool @@ -172,7 +166,7 @@ final class AIWebProvider implements InfoProviderInterface $llmResponse = $this->callLLM($markdown, $url, $structuredData); // Build and return PartDetailDTO - $result = $this->jsonSchemaConverter->jsonToDTO($llmResponse, self::PROVIDER_KEY, $url, $url, self::DISTRIBUTOR_NAME); + $result = $this->jsonSchemaConverter->jsonToDTO($llmResponse, $this->getProviderKey(), $url, $url, self::DISTRIBUTOR_NAME); // Cache the result for future use, to improve performance and reduce costs. $cacheItem->set($result); @@ -262,6 +256,17 @@ 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 9197df48..ca6e26e1 100644 --- a/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php +++ b/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php @@ -28,7 +28,6 @@ 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; @@ -41,7 +40,6 @@ 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; /** @@ -177,24 +175,20 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv } - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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 - ], - ); + return [ + 'name' => 'Buerklin', + 'description' => 'This provider uses the Buerklin API to search for parts.', + 'url' => 'https://www.buerklin.com/', + 'disabled_help' => 'Configure the API Client ID, Secret, Username and Password provided by Buerklin in the provider settings to enable.', + 'settings_class' => BuerklinSettings::class + ]; + } + + public function getProviderKey(): string + { + return 'buerklin'; } // This provider is considered active if settings are present @@ -289,7 +283,7 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv } return new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: (string) ($product['code'] ?? $code), name: (string) ($product['manufacturerProductId'] ?? $code), @@ -515,6 +509,17 @@ 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 3390bd98..aee30d6b 100644 --- a/src/Services/InfoProviderSystem/Providers/CanopyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/CanopyProvider.php @@ -26,7 +26,6 @@ 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; @@ -40,7 +39,6 @@ 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"; @@ -54,22 +52,20 @@ class CanopyProvider implements InfoProviderInterface } - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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, - ); + 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'; } public function isActive(): bool @@ -135,7 +131,7 @@ class CanopyProvider implements InfoProviderInterface $dto = new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $result['asin'], name: $result["title"], description: "", @@ -213,7 +209,7 @@ class CanopyProvider implements InfoProviderInterface } return new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $product['asin'], name: $product['title'], description: '', @@ -226,4 +222,12 @@ 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 0fb1525f..2e6708be 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -27,7 +27,6 @@ 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; @@ -39,7 +38,6 @@ 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; @@ -53,25 +51,20 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr ]); } - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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 - ], - ); + 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'; } public function isActive(): bool @@ -118,7 +111,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr foreach($results['hits'] as $result) { $out[] = new SearchResultDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $result['productId'], name: $result['manufacturerId'] ?? $result['productId'], description: $result['title'] ?? '', @@ -300,7 +293,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr $data = $response->toArray(); return new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $data['shortProductNumber'], name: $data['productFullInformation']['manufacturer']['name'] ?? $data['productFullInformation']['manufacturer']['id'] ?? $data['shortProductNumber'], description: $data['productShortInformation']['title'] ?? '', @@ -318,6 +311,18 @@ 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 fedda225..e7a62aa4 100644 --- a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php @@ -29,7 +29,6 @@ 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; @@ -38,7 +37,6 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; class DigikeyProvider implements InfoProviderInterface { - public const PROVIDER_KEY = 'digikey'; private const OAUTH_APP_NAME = 'ip_digikey_oauth'; @@ -74,25 +72,32 @@ class DigikeyProvider implements InfoProviderInterface ]); } - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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 - ], - ); + 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'; } public function isActive(): bool @@ -123,7 +128,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(self::PROVIDER_KEY); + throw OAuthReconnectRequiredException::forProvider($this->getProviderKey()); } throw $exception; @@ -136,7 +141,7 @@ class DigikeyProvider implements InfoProviderInterface foreach ($products as $product) { foreach ($product['ProductVariations'] as $variation) { $result[] = new SearchResultDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $variation['DigiKeyProductNumber'], name: $product['ManufacturerProductNumber'], description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'], @@ -163,7 +168,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(self::PROVIDER_KEY); + throw OAuthReconnectRequiredException::forProvider($this->getProviderKey()); } throw $exception; @@ -186,7 +191,7 @@ class DigikeyProvider implements InfoProviderInterface } return new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), 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 ba8a7370..1d9e092c 100644 --- a/src/Services/InfoProviderSystem/Providers/Element14Provider.php +++ b/src/Services/InfoProviderSystem/Providers/Element14Provider.php @@ -28,7 +28,6 @@ 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; @@ -36,7 +35,6 @@ 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'; @@ -62,23 +60,20 @@ class Element14Provider implements InfoProviderInterface, URLHandlerInfoProvider ]); } - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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, - ], - ); + 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'; } public function isActive(): bool @@ -118,7 +113,7 @@ class Element14Provider implements InfoProviderInterface, URLHandlerInfoProvider foreach ($products as $product) { $result[] = new PartDetailDTO( - provider_key: self::PROVIDER_KEY, provider_id: $product['sku'], + provider_key: $this->getProviderKey(), provider_id: $product['sku'], name: $product['translatedManufacturerPartNumber'], description: $this->displayNameToDescription($product['displayName'], $product['translatedManufacturerPartNumber']), manufacturer: $product['vendorName'] ?? $product['brandName'] ?? null, @@ -306,6 +301,15 @@ 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 eea14ed8..915a118c 100644 --- a/src/Services/InfoProviderSystem/Providers/EmptyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/EmptyProvider.php @@ -25,7 +25,6 @@ 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; @@ -35,20 +34,19 @@ use Symfony\Component\DependencyInjection\Attribute\When; #[When(env: 'test')] class EmptyProvider implements InfoProviderInterface { - public const PROVIDER_KEY = 'empty'; - - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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, - ], - ); + return [ + 'name' => 'Empty 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 'empty'; } public function isActive(): bool @@ -63,6 +61,14 @@ 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 420049b3..45777f9e 100644 --- a/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php +++ b/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php @@ -30,7 +30,6 @@ 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; @@ -54,7 +53,6 @@ class GenericWebProvider implements InfoProviderInterface use FixAndValidateUrlTrait; public const DISTRIBUTOR_NAME = 'Website'; - public const PROVIDER_KEY = 'generic_web'; private readonly HttpClientInterface $httpClient; @@ -71,21 +69,20 @@ class GenericWebProvider implements InfoProviderInterface ); } - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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, - ], - ); + 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'; } public function isActive(): bool @@ -230,7 +227,7 @@ class GenericWebProvider implements InfoProviderInterface } return new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), 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') ?? '', @@ -379,7 +376,7 @@ class GenericWebProvider implements InfoProviderInterface )]; return new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $canonicalURL, name: $this->getMetaContent($dom, 'og:title') ?? $pageTitle, description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '', @@ -390,4 +387,13 @@ 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 cdc8086c..d3895795 100644 --- a/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php +++ b/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php @@ -24,7 +24,6 @@ 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 @@ -35,8 +34,26 @@ 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(): ProviderInfoDTO; + 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; /** * Checks if this provider is enabled or not (meaning that it can be used for searching) @@ -59,4 +76,12 @@ 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 de1906cc..b3da3cda 100755 --- a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php +++ b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php @@ -28,7 +28,6 @@ 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; @@ -40,31 +39,26 @@ 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(): ProviderInfoDTO + public function getProviderInfo(): array { - 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 - ], - ); + 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'; } // This provider is always active @@ -233,7 +227,7 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider } return new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $product['productCode'], name: $product['productModel'], description: $this->sanitizeField($product['productIntroEn']) ?? '', @@ -461,6 +455,17 @@ 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 c1b91faa..49ca2d50 100644 --- a/src/Services/InfoProviderSystem/Providers/MouserProvider.php +++ b/src/Services/InfoProviderSystem/Providers/MouserProvider.php @@ -36,7 +36,6 @@ 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; @@ -49,7 +48,6 @@ 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, @@ -57,22 +55,20 @@ class MouserProvider implements InfoProviderInterface ) { } - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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, - ], - ); + 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'; } public function isActive(): bool @@ -211,6 +207,17 @@ 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[] @@ -244,7 +251,7 @@ class MouserProvider implements InfoProviderInterface $result[] = new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), 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 edeb5f71..9764517b 100644 --- a/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php +++ b/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php @@ -88,7 +88,6 @@ 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; @@ -97,7 +96,6 @@ use Psr\Cache\CacheItemPoolInterface; class OEMSecretsProvider implements InfoProviderInterface { - public const PROVIDER_KEY = 'oemsecrets'; private const ENDPOINT_URL = 'https://oemsecretsapi.com/partsearch'; @@ -229,23 +227,37 @@ class OEMSecretsProvider implements InfoProviderInterface private array $distributorCountryCodes = []; private array $countryCodeToRegionMap = []; - public function getProviderInfo(): ProviderInfoDTO + /** + * 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 { - 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 - ], - ); + 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'; } /** @@ -444,6 +456,17 @@ 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`. @@ -873,7 +896,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' => self::PROVIDER_KEY, + 'provider_key' => $this->getProviderKey(), 'provider_id' => $provider_id, 'name' => $product['part_number'], 'description' => $description, @@ -893,7 +916,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'] ?? self::PROVIDER_KEY, + 'provider_key' => $existingBasicInfo['provider_key'] ?? $this->getProviderKey(), 'provider_id' => $existingBasicInfo['provider_id'] ?? $provider_id, 'name' => $existingBasicInfo['name'] ?? $product['part_number'], // Update description if it's null/empty @@ -1247,7 +1270,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 14fdbfa5..de404e18 100644 --- a/src/Services/InfoProviderSystem/Providers/OctopartProvider.php +++ b/src/Services/InfoProviderSystem/Providers/OctopartProvider.php @@ -28,7 +28,6 @@ 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; @@ -44,8 +43,6 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; */ class OctopartProvider implements InfoProviderInterface { - public const PROVIDER_KEY = 'octopart'; - private const OAUTH_APP_NAME = 'ip_octopart_oauth'; /** @@ -167,25 +164,20 @@ class OctopartProvider implements InfoProviderInterface return $response->toArray(true); } - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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, - ); + 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'; } public function isActive(): bool @@ -315,7 +307,7 @@ class OctopartProvider implements InfoProviderInterface } return new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $part['id'], name: $part['mpn'], description: $part['shortDescription'] ?? null, @@ -405,4 +397,14 @@ 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 919db66b..7acecc3a 100644 --- a/src/Services/InfoProviderSystem/Providers/PollinProvider.php +++ b/src/Services/InfoProviderSystem/Providers/PollinProvider.php @@ -29,7 +29,6 @@ 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; @@ -39,7 +38,6 @@ 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, @@ -47,23 +45,20 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt { } - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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 - ], - ); + 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'; } public function isActive(): bool @@ -93,7 +88,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: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $node->filter('meta[itemprop="productID"]')->attr('content'), name: $node->filter('a.product-name')->text(), description: '', @@ -161,7 +156,7 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt $purchaseInfo = new PurchaseInfoDTO('Pollin', $orderId, $this->parsePrices($dom), $productPageUrl); return new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $orderId, name: trim($dom->filter('meta[property="og:title"]')->attr('content')), description: $dom->filter('meta[property="og:description"]')->attr('content'), @@ -249,6 +244,16 @@ 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 a97aa347..3a7d03e9 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: string +enum ProviderCapabilities { /** Basic information about a part, like the name, description, part number, manufacturer etc */ - case BASIC = 'BASIC'; + case BASIC; /** Provider can provide a picture for a part */ - case PICTURE = 'PICTURE'; + case PICTURE; /** Provider can provide datasheets for a part */ - case DATASHEET = 'DATASHEET'; + case DATASHEET; /** Provider can provide prices for a part */ - case PRICE = 'PRICE'; + case PRICE; /** Information about the footprint of a part */ - case FOOTPRINT = 'FOOTPRINT'; + case FOOTPRINT; /** Provider can provide GTIN for a part */ - case GTIN = 'GTIN'; + case GTIN; /** Provider can provide parameters/specifications for a part */ - case PARAMETERS = 'PARAMETERS'; + case 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 901956e1..9dfb099f 100644 --- a/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php @@ -28,7 +28,6 @@ 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; @@ -39,7 +38,6 @@ class ReicheltProvider implements InfoProviderInterface { public const DISTRIBUTOR_NAME = "Reichelt"; - public const PROVIDER_KEY = 'reichelt'; private readonly HttpClientInterface $client; @@ -50,24 +48,20 @@ class ReicheltProvider implements InfoProviderInterface $this->client = new RandomizeUseragentHttpClient($client); } - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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 - ], - ); + 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'; } public function isActive(): bool @@ -99,7 +93,7 @@ class ReicheltProvider implements InfoProviderInterface $pictureURL = $element->filter("div.al_artlogo img")->attr('src'); $results[] = new SearchResultDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $artId, name: $productID, description: $name, @@ -179,7 +173,7 @@ class ReicheltProvider implements InfoProviderInterface //Create part object return new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $id, name: $json[0]['article_artnr'], description: $json[0]['article_besch'], @@ -288,4 +282,14 @@ 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 3d230a26..24ba0ea7 100644 --- a/src/Services/InfoProviderSystem/Providers/TMEProvider.php +++ b/src/Services/InfoProviderSystem/Providers/TMEProvider.php @@ -28,14 +28,12 @@ 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'; @@ -50,24 +48,20 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf } } - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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 - ], - ); + 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'; } public function isActive(): bool @@ -89,7 +83,7 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf foreach($data['ProductList'] as $product) { $result[] = new SearchResultDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $product['Symbol'], name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'], description: $product['Description'], @@ -125,7 +119,7 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf $parameters = $this->getParameters($id, $footprint); return new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $product['Symbol'], name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'], description: $product['Description'], @@ -296,6 +290,17 @@ 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 f4528d34..42927abd 100644 --- a/src/Services/InfoProviderSystem/Providers/TestProvider.php +++ b/src/Services/InfoProviderSystem/Providers/TestProvider.php @@ -25,7 +25,6 @@ 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; @@ -35,20 +34,20 @@ use Symfony\Component\DependencyInjection\Attribute\When; #[When(env: 'test')] class TestProvider implements InfoProviderInterface { - public const PROVIDER_KEY = 'test'; - public function getProviderInfo(): ProviderInfoDTO + public function getProviderInfo(): array { - 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, - ], - ); + 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'; } public function isActive(): bool @@ -59,16 +58,24 @@ class TestProvider implements InfoProviderInterface public function searchByKeyword(string $keyword, array $options = []): array { return [ - 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'), + 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, ]; } public function getDetails(string $id, array $options = []): PartDetailDTO { return new PartDetailDTO( - provider_key: self::PROVIDER_KEY, + provider_key: $this->getProviderKey(), provider_id: $id, name: 'Test Element', description: 'fd', diff --git a/src/State/Mcp/ListInfoProvidersProcessor.php b/src/State/Mcp/ListInfoProvidersProcessor.php index 6b995a15..774ad4ce 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\ProviderInfoDTO; +use App\Services\InfoProviderSystem\DTOs\InfoProviderDTO; use App\Services\InfoProviderSystem\ProviderRegistry; /** @@ -40,7 +40,7 @@ class ListInfoProvidersProcessor implements ProcessorInterface, ProviderInterfac } /** - * @return ProviderInfoDTO[] + * @return InfoProviderDTO[] */ public function provide(Operation $operation, array $uriVariables = [], array $context = []): array { @@ -48,7 +48,7 @@ class ListInfoProvidersProcessor implements ProcessorInterface, ProviderInterfac } /** - * @return ProviderInfoDTO[] + * @return InfoProviderDTO[] */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): array { @@ -56,14 +56,22 @@ class ListInfoProvidersProcessor implements ProcessorInterface, ProviderInterfac } /** - * @return ProviderInfoDTO[] + * @return InfoProviderDTO[] */ private function listActiveProviders(): array { $result = []; foreach ($this->providerRegistry->getActiveProviders() as $provider) { - $result[] = $provider->getProviderInfo(); + $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()), + ); } return $result; diff --git a/src/Twig/InfoProviderExtension.php b/src/Twig/InfoProviderExtension.php index 4de13115..54dbf93a 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 ad03f20a..bec8f24b 100644 --- a/templates/info_providers/providers.macro.html.twig +++ b/templates/info_providers/providers.macro.html.twig @@ -8,41 +8,35 @@
- {% if provider.providerInfo.url is not empty %} + {% if provider.providerInfo.url is defined and provider.providerInfo.url is not empty %} {{ provider.providerInfo.name }} {% else %} - {{ provider.providerInfo.name }} + {{ provider.providerInfo.name | trans }} {% endif %}
- {% if provider.providerInfo.description is not null %} - {{ provider.providerInfo.description }} + {% if provider.providerInfo.description is defined and provider.providerInfo.description is not null %} + {{ provider.providerInfo.description | trans }} {% endif %}
- {% if provider.providerInfo.settingsClass is not null %} - {% endif %} - {% if provider.providerInfo.expensive %} - - - {% trans %}info_providers.expensive{% endtrans %} - - {% endif %} - {% for capability in provider.providerInfo.capabilities|sort((a, b) => a.orderIndex <=> b.orderIndex) %} + {% for capability in provider.capabilities|sort((a, b) => a.orderIndex <=> b.orderIndex) %} {# @var capability \App\Services\InfoProviderSystem\Providers\ProviderCapabilities #} {{ capability.translationKey|trans }} {% endfor %} - {% if provider.providerInfo.oauthAppName is not empty %} + {% if provider.providerInfo.oauth_app_name is defined and provider.providerInfo.oauth_app_name is not empty %}
- {% trans %}oauth_client.connect.btn{% endtrans %} + {% trans %}oauth_client.connect.btn{% endtrans %} {% endif %}
@@ -50,9 +44,9 @@
{% trans %}info_providers.providers_list.disabled{% endtrans %} - {% if provider.providerInfo.disabledHelp is not empty %} + {% if provider.providerInfo.disabled_help is defined and provider.providerInfo.disabled_help is not empty %}
- {{ provider.providerInfo.disabledHelp }} + {{ provider.providerInfo.disabled_help|trans }} {% endif %}
diff --git a/templates/info_providers/settings/provider_settings.html.twig b/templates/info_providers/settings/provider_settings.html.twig index 1159fbf3..db942f8a 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 not empty %} + {% if info_provider_info.url is defined %} {{ info_provider_info.name }} {% else %} {{ info_provider_info.name }} diff --git a/tests/API/Endpoints/InfoProviderEndpointTest.php b/tests/API/Endpoints/InfoProviderEndpointTest.php index fc89ad0d..7203b15c 100644 --- a/tests/API/Endpoints/InfoProviderEndpointTest.php +++ b/tests/API/Endpoints/InfoProviderEndpointTest.php @@ -39,16 +39,6 @@ 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 a8d87dc5..2fd50f9a 100644 --- a/tests/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTOTest.php +++ b/tests/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTOTest.php @@ -24,7 +24,6 @@ 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 @@ -33,7 +32,7 @@ final class BulkSearchFieldMappingDTOTest extends TestCase public function testProviderInstanceNormalization(): void { $mockProvider = $this->createMock(InfoProviderInterface::class); - $mockProvider->method('getProviderInfo')->willReturn(new ProviderInfoDTO(key: 'mock_provider', name: 'mock_provider')); + $mockProvider->method('getProviderKey')->willReturn('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 3043edff..d3fce441 100644 --- a/tests/Services/InfoProviderSystem/ProviderRegistryTest.php +++ b/tests/Services/InfoProviderSystem/ProviderRegistryTest.php @@ -22,7 +22,6 @@ 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; @@ -47,7 +46,7 @@ final class ProviderRegistryTest extends TestCase public function getMockProvider(string $key, bool $active = true): InfoProviderInterface { $mock = $this->createMockForIntersectionOfInterfaces([InfoProviderInterface::class, URLHandlerInfoProviderInterface::class]); - $mock->method('getProviderInfo')->willReturn(new ProviderInfoDTO(key: $key, name: $key)); + $mock->method('getProviderKey')->willReturn($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 bce0a2cf..63e1b452 100644 --- a/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php +++ b/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php @@ -27,7 +27,6 @@ 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; @@ -57,10 +56,18 @@ final class LCSCProviderTest extends TestCase { $info = $this->provider->getProviderInfo(); - $this->assertInstanceOf(ProviderInfoDTO::class, $info); - $this->assertSame('lcsc', $info->key); - $this->assertEquals('LCSC', $info->name); - $this->assertEquals('https://www.lcsc.com/', $info->url); + $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()); } public function testIsActiveWhenEnabled(): void @@ -79,7 +86,7 @@ final class LCSCProviderTest extends TestCase public function testGetCapabilities(): void { - $capabilities = $this->provider->getProviderInfo()->capabilities; + $capabilities = $this->provider->getCapabilities(); $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 be4629e5..20fba523 100644 --- a/tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php +++ b/tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php @@ -24,7 +24,6 @@ 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; @@ -213,10 +212,17 @@ final class TMEProviderTest extends TestCase { $info = $this->provider->getProviderInfo(); - $this->assertInstanceOf(ProviderInfoDTO::class, $info); - $this->assertSame('tme', $info->key); - $this->assertEquals('TME', $info->name); - $this->assertEquals('https://tme.eu/', $info->url); + $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()); } public function testIsActiveWithCredentials(): void @@ -233,7 +239,7 @@ final class TMEProviderTest extends TestCase public function testGetCapabilities(): void { - $capabilities = $this->provider->getProviderInfo()->capabilities; + $capabilities = $this->provider->getCapabilities(); $this->assertIsArray($capabilities); $this->assertContains(ProviderCapabilities::BASIC, $capabilities); diff --git a/tests/State/Mcp/GetInfoProviderPartDetailsProcessorTest.php b/tests/State/Mcp/GetInfoProviderPartDetailsProcessorTest.php index 2f6e61ba..07fcdc27 100644 --- a/tests/State/Mcp/GetInfoProviderPartDetailsProcessorTest.php +++ b/tests/State/Mcp/GetInfoProviderPartDetailsProcessorTest.php @@ -26,7 +26,6 @@ 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; @@ -46,14 +45,14 @@ final class GetInfoProviderPartDetailsProcessorTest extends TestCase protected function setUp(): void { $activeProvider = $this->createMock(InfoProviderInterface::class); - $activeProvider->method('getProviderInfo')->willReturn(new ProviderInfoDTO(key: 'test1', name: 'test1')); + $activeProvider->method('getProviderKey')->willReturn('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('getProviderInfo')->willReturn(new ProviderInfoDTO(key: 'test2', name: 'test2')); + $inactiveProvider->method('getProviderKey')->willReturn('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 b37e85f4..a255aee3 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\ProviderInfoDTO; +use App\Services\InfoProviderSystem\DTOs\InfoProviderDTO; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use App\Services\InfoProviderSystem\Providers\ProviderCapabilities; use App\Services\InfoProviderSystem\ProviderRegistry; @@ -37,18 +37,20 @@ 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(new ProviderInfoDTO( - key: 'active_provider', - name: 'Active Provider', - description: 'A provider that is active', - url: 'https://example.com', - capabilities: [ProviderCapabilities::BASIC, ProviderCapabilities::PRICE], - )); + $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]); $disabled = $this->createMock(InfoProviderInterface::class); + $disabled->method('getProviderKey')->willReturn('disabled_provider'); $disabled->method('isActive')->willReturn(false); - $disabled->method('getProviderInfo')->willReturn(new ProviderInfoDTO(key: 'disabled_provider', name: 'Disabled Provider')); + $disabled->method('getProviderInfo')->willReturn(['name' => 'Disabled Provider']); + $disabled->method('getCapabilities')->willReturn([]); $registry = new ProviderRegistry([$active, $disabled]); $processor = new ListInfoProvidersProcessor($registry); @@ -56,11 +58,11 @@ final class ListInfoProvidersProcessorTest extends TestCase $result = $processor->process(new ListInfoProvidersInput(), new Get()); $this->assertCount(1, $result); - $this->assertInstanceOf(ProviderInfoDTO::class, $result[0]); + $this->assertInstanceOf(InfoProviderDTO::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([ProviderCapabilities::BASIC, ProviderCapabilities::PRICE], $result[0]->capabilities); + $this->assertSame(['BASIC', 'PRICE'], $result[0]->capabilities); } } diff --git a/tests/State/Mcp/SearchInfoProvidersProcessorTest.php b/tests/State/Mcp/SearchInfoProvidersProcessorTest.php index 52c24832..2a56a9a2 100644 --- a/tests/State/Mcp/SearchInfoProvidersProcessorTest.php +++ b/tests/State/Mcp/SearchInfoProvidersProcessorTest.php @@ -25,7 +25,6 @@ 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; @@ -70,8 +69,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'), ]); diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 597816e1..ccf977e3 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13805,23 +13805,5 @@ Buerklin-API Authentication server: MCP endpoint - - - info_providers.capabilities.parameters - Parameters - - - - - info_providers.expensive - Expensive - - - - - info_providers.expensive.tooltip - Using this info provider can cause additional costs, or has very strict rate limits. - -