Compare commits

..

No commits in common. "51ce453d1a2d725fe61184fcb87a95b813e6d241" and "661cc5a052f76d071686587ebe69cad8f1c9bdbe" have entirely different histories.

42 changed files with 764 additions and 526 deletions

View file

@ -73,7 +73,7 @@ class BrowserPluginController extends AbstractController
if (isset($activeProviders[$key])) { if (isset($activeProviders[$key])) {
$urlProviders[] = [ $urlProviders[] = [
'id' => $key, 'id' => $key,
'label' => $activeProviders[$key]->getProviderInfo()->name, 'label' => $activeProviders[$key]->getProviderInfo()['name'],
]; ];
} }
} }

View file

@ -88,7 +88,7 @@ class InfoProviderController extends AbstractController
$this->denyAccessUnlessGranted('@info_providers.create_parts'); $this->denyAccessUnlessGranted('@info_providers.create_parts');
$providerInstance = $this->providerRegistry->getProviderByKey($provider); $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 //Create a clone of the settings object
$settings = $this->settingsManager->createTemporaryCopy($settingsClass); $settings = $this->settingsManager->createTemporaryCopy($settingsClass);

View file

@ -43,7 +43,6 @@ class InfoProviderNotActiveException extends \RuntimeException
*/ */
public static function fromProvider(InfoProviderInterface $provider): self public static function fromProvider(InfoProviderInterface $provider): self
{ {
$info = $provider->getProviderInfo(); return new self($provider->getProviderKey(), $provider->getProviderInfo()['name'] ?? '???');
return new self($info->key, $info->name);
} }
} }

View file

@ -66,8 +66,8 @@ class ProviderSelectType extends AbstractType
$tmp = []; $tmp = [];
foreach ($providers as $provider) { foreach ($providers as $provider) {
$info = $provider->getProviderInfo(); $name = $provider->getProviderInfo()['name'];
$tmp[$info->name] = $info->key; $tmp[$name] = $provider->getProviderKey();
} }
return $tmp; return $tmp;
@ -77,7 +77,7 @@ class ProviderSelectType extends AbstractType
$resolver->setDefault('choice_label', function (Options $options) { $resolver->setDefault('choice_label', function (Options $options) {
if ('object' === $options['input']) { if ('object' === $options['input']) {
return ChoiceList::label($this, static fn(?InfoProviderInterface $choice 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); return static fn($choice, $key, $value) => new StaticMessage($key);
@ -85,7 +85,7 @@ class ProviderSelectType extends AbstractType
$resolver->setDefault('choice_value', function (Options $options) { $resolver->setDefault('choice_value', function (Options $options) {
if ('object' === $options['input']) { if ('object' === $options['input']) {
return ChoiceList::value($this, return ChoiceList::value($this,
static fn(?InfoProviderInterface $choice) => $choice?->getProviderInfo()->key); static fn(?InfoProviderInterface $choice) => $choice?->getProviderKey());
} }
return null; return null;

View file

@ -0,0 +1,162 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\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;
}
}

View file

@ -72,7 +72,7 @@ final readonly class CreateFromUrlHelper
$provider = $this->providerRegistry->getProviderHandlingDomain($host); $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 { try {
$id = $provider->getIDFromURL($url); $id = $provider->getIDFromURL($url);
if ($id !== null) { if ($id !== null) {

View file

@ -49,7 +49,7 @@ readonly class BulkSearchFieldMappingDTO
//Ensure that providers are provided as keys //Ensure that providers are provided as keys
foreach ($providers as &$provider) { foreach ($providers as &$provider) {
if ($provider instanceof InfoProviderInterface) { if ($provider instanceof InfoProviderInterface) {
$provider = $provider->getProviderInfo()->key; $provider = $provider->getProviderKey();
} }
if (!is_string($provider)) { if (!is_string($provider)) {
throw new \InvalidArgumentException('Providers must be provided as strings or InfoProviderInterface instances'); throw new \InvalidArgumentException('Providers must be provided as strings or InfoProviderInterface instances');

View file

@ -0,0 +1,73 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Services\InfoProviderSystem\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 = [],
) {
}
}

View file

@ -1,104 +0,0 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Services\InfoProviderSystem\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,
) {
}
}

View file

@ -103,7 +103,7 @@ final class PartInfoRetriever
//Generate a hash for the options, to ensure that different options result in different cache entries //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)); $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 is set, bypass the cache and get fresh results from the provider
if ($no_cache) { if ($no_cache) {

View file

@ -72,7 +72,7 @@ final class ProviderRegistry
private function initStructures(): void private function initStructures(): void
{ {
foreach ($this->providers as $provider) { foreach ($this->providers as $provider) {
$key = $provider->getProviderInfo()->key; $key = $provider->getProviderKey();
if (isset($this->providers_by_name[$key])) { if (isset($this->providers_by_name[$key])) {
throw new \LogicException("Provider with key $key already registered"); throw new \LogicException("Provider with key $key already registered");

View file

@ -31,7 +31,6 @@ use App\Services\InfoProviderSystem\SubmittedPageStorage;
use App\Services\InfoProviderSystem\CreateFromUrlHelper; use App\Services\InfoProviderSystem\CreateFromUrlHelper;
use App\Services\InfoProviderSystem\DTOJsonSchemaConverter; use App\Services\InfoProviderSystem\DTOJsonSchemaConverter;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Settings\InfoProviderSystem\AIExtractorSettings; use App\Settings\InfoProviderSystem\AIExtractorSettings;
use Jkphl\Micrometa; use Jkphl\Micrometa;
use League\HTMLToMarkdown\HtmlConverter; use League\HTMLToMarkdown\HtmlConverter;
@ -51,8 +50,6 @@ final class AIWebProvider implements InfoProviderInterface
{ {
use FixAndValidateUrlTrait; use FixAndValidateUrlTrait;
public const PROVIDER_KEY = 'ai_web';
private const DISTRIBUTOR_NAME = 'Website'; private const DISTRIBUTOR_NAME = 'Website';
private readonly HttpClientInterface $httpClient; private readonly HttpClientInterface $httpClient;
@ -74,23 +71,20 @@ final class AIWebProvider implements InfoProviderInterface
); );
} }
public function getProviderInfo(): ProviderInfoDTO public function getProviderInfo(): array
{ {
return new ProviderInfoDTO( return [
key: self::PROVIDER_KEY, 'name' => 'AI Web Extractor',
name: 'AI Web Extractor', 'description' => 'Extract part info from any URL using LLM',
description: 'Extract part info from any URL using LLM', //'url' => 'https://openrouter.ai',
disabledHelp: 'Configure AI settings', 'disabled_help' => 'Configure AI settings',
settingsClass: AIExtractorSettings::class, 'settings_class' => AIExtractorSettings::class,
capabilities: [ ];
ProviderCapabilities::BASIC, }
ProviderCapabilities::PICTURE,
ProviderCapabilities::DATASHEET, public function getProviderKey(): string
ProviderCapabilities::PRICE, {
ProviderCapabilities::PARAMETERS, return 'ai_web';
],
expensive: true,
);
} }
public function isActive(): bool public function isActive(): bool
@ -172,7 +166,7 @@ final class AIWebProvider implements InfoProviderInterface
$llmResponse = $this->callLLM($markdown, $url, $structuredData); $llmResponse = $this->callLLM($markdown, $url, $structuredData);
// Build and return PartDetailDTO // 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. // Cache the result for future use, to improve performance and reduce costs.
$cacheItem->set($result); $cacheItem->set($result);
@ -262,6 +256,17 @@ final class AIWebProvider implements InfoProviderInterface
return $converter->convert($htmlToConvert); 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 private function callLLM(string $htmlContent, string $url, ?string $structuredData = null): array
{ {
$input = new MessageBag( $input = new MessageBag(

View file

@ -28,7 +28,6 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\BuerklinSettings; 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'; private const ENDPOINT_URL = 'https://www.buerklin.com/buerklinws/v2/buerklin';
public const DISTRIBUTOR_NAME = 'Buerklin'; public const DISTRIBUTOR_NAME = 'Buerklin';
public const PROVIDER_KEY = 'buerklin';
private const CACHE_TTL = 600; 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( return [
key: self::PROVIDER_KEY, 'name' => 'Buerklin',
name: 'Buerklin', 'description' => 'This provider uses the Buerklin API to search for parts.',
description: 'This provider uses the Buerklin API to search for parts.', 'url' => 'https://www.buerklin.com/',
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.',
disabledHelp: 'Configure the API Client ID, Secret, Username and Password provided by Buerklin in the provider settings to enable.', 'settings_class' => BuerklinSettings::class
settingsClass: BuerklinSettings::class, ];
capabilities: [ }
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE, public function getProviderKey(): string
//ProviderCapabilities::DATASHEET, // currently not implemented {
ProviderCapabilities::PRICE, return 'buerklin';
ProviderCapabilities::FOOTPRINT,
ProviderCapabilities::PARAMETERS
],
);
} }
// This provider is considered active if settings are present // This provider is considered active if settings are present
@ -289,7 +283,7 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv
} }
return new PartDetailDTO( return new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: (string) ($product['code'] ?? $code), provider_id: (string) ($product['code'] ?? $code),
name: (string) ($product['manufacturerProductId'] ?? $code), name: (string) ($product['manufacturerProductId'] ?? $code),
@ -515,6 +509,17 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv
return $this->getPartDetail($response); 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 private function complianceToParameters(array $product, ?string $group = 'Compliance'): array
{ {
$params = []; $params = [];

View file

@ -26,7 +26,6 @@ namespace App\Services\InfoProviderSystem\Providers;
use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\BuerklinSettings; use App\Settings\InfoProviderSystem\BuerklinSettings;
@ -40,7 +39,6 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*/ */
class CanopyProvider implements InfoProviderInterface class CanopyProvider implements InfoProviderInterface
{ {
public const PROVIDER_KEY = 'canopy';
public const BASE_URL = "https://rest.canopyapi.co/api"; public const BASE_URL = "https://rest.canopyapi.co/api";
public const SEARCH_API_URL = self::BASE_URL . "/amazon/search"; 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( return [
key: self::PROVIDER_KEY, 'name' => 'Amazon (Canopy)',
name: 'Amazon (Canopy)', 'description' => 'Retrieves part infos from Amazon using the Canopy API',
description: 'Retrieves part infos from Amazon using the Canopy API', 'url' => 'https://canopyapi.co',
url: 'https://canopyapi.co', 'disabled_help' => 'Set Canopy API key in the provider configuration to enable this provider',
disabledHelp: 'Set Canopy API key in the provider configuration to enable this provider', 'settings_class' => CanopySettings::class
settingsClass: CanopySettings::class, ];
capabilities: [ }
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE, public function getProviderKey(): string
ProviderCapabilities::PRICE, {
], return 'canopy';
expensive: true,
);
} }
public function isActive(): bool public function isActive(): bool
@ -135,7 +131,7 @@ class CanopyProvider implements InfoProviderInterface
$dto = new PartDetailDTO( $dto = new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $result['asin'], provider_id: $result['asin'],
name: $result["title"], name: $result["title"],
description: "", description: "",
@ -213,7 +209,7 @@ class CanopyProvider implements InfoProviderInterface
} }
return new PartDetailDTO( return new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $product['asin'], provider_id: $product['asin'],
name: $product['title'], name: $product['title'],
description: '', description: '',
@ -226,4 +222,12 @@ class CanopyProvider implements InfoProviderInterface
); );
} }
public function getCapabilities(): array
{
return [
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE,
ProviderCapabilities::PRICE,
];
}
} }

View file

@ -27,7 +27,6 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\ConradSettings; use App\Settings\InfoProviderSystem\ConradSettings;
@ -39,7 +38,6 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr
private const SEARCH_ENDPOINT = '/search/1/v3/facetSearch'; private const SEARCH_ENDPOINT = '/search/1/v3/facetSearch';
public const DISTRIBUTOR_NAME = 'Conrad'; public const DISTRIBUTOR_NAME = 'Conrad';
public const PROVIDER_KEY = 'conrad';
private HttpClientInterface $httpClient; private HttpClientInterface $httpClient;
@ -53,25 +51,20 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr
]); ]);
} }
public function getProviderInfo(): ProviderInfoDTO public function getProviderInfo(): array
{ {
return new ProviderInfoDTO( return [
key: self::PROVIDER_KEY, 'name' => 'Conrad',
name: 'Conrad', 'description' => 'Retrieves part information from conrad.de',
description: 'Retrieves part information from conrad.de', 'url' => 'https://www.conrad.de/',
url: 'https://www.conrad.de/', 'disabled_help' => 'Set API key in settings',
disabledHelp: 'Set API key in settings', 'settings_class' => ConradSettings::class,
settingsClass: ConradSettings::class, ];
capabilities: [ }
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE, public function getProviderKey(): string
ProviderCapabilities::DATASHEET, {
ProviderCapabilities::PRICE, return 'conrad';
ProviderCapabilities::FOOTPRINT,
ProviderCapabilities::GTIN,
ProviderCapabilities::PARAMETERS
],
);
} }
public function isActive(): bool public function isActive(): bool
@ -118,7 +111,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr
foreach($results['hits'] as $result) { foreach($results['hits'] as $result) {
$out[] = new SearchResultDTO( $out[] = new SearchResultDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $result['productId'], provider_id: $result['productId'],
name: $result['manufacturerId'] ?? $result['productId'], name: $result['manufacturerId'] ?? $result['productId'],
description: $result['title'] ?? '', description: $result['title'] ?? '',
@ -300,7 +293,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr
$data = $response->toArray(); $data = $response->toArray();
return new PartDetailDTO( return new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $data['shortProductNumber'], provider_id: $data['shortProductNumber'],
name: $data['productFullInformation']['manufacturer']['name'] ?? $data['productFullInformation']['manufacturer']['id'] ?? $data['shortProductNumber'], name: $data['productFullInformation']['manufacturer']['name'] ?? $data['productFullInformation']['manufacturer']['id'] ?? $data['shortProductNumber'],
description: $data['productShortInformation']['title'] ?? '', 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 public function getHandledDomains(): array
{ {
$domains = []; $domains = [];

View file

@ -29,7 +29,6 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Services\OAuth\OAuthTokenManager; use App\Services\OAuth\OAuthTokenManager;
@ -38,7 +37,6 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
class DigikeyProvider implements InfoProviderInterface class DigikeyProvider implements InfoProviderInterface
{ {
public const PROVIDER_KEY = 'digikey';
private const OAUTH_APP_NAME = 'ip_digikey_oauth'; 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( return [
key: self::PROVIDER_KEY, 'name' => 'DigiKey',
name: 'DigiKey', 'description' => 'This provider uses the DigiKey API to search for parts.',
description: 'This provider uses the DigiKey API to search for parts.', 'url' => 'https://www.digikey.com/',
url: 'https://www.digikey.com/', 'oauth_app_name' => self::OAUTH_APP_NAME,
disabledHelp: 'Set the Client ID and Secret in provider settings and connect OAuth to enable.', 'disabled_help' => 'Set the Client ID and Secret in provider settings and connect OAuth to enable.',
oauthAppName: self::OAUTH_APP_NAME, 'settings_class' => DigikeySettings::class,
settingsClass: DigikeySettings::class, ];
capabilities: [ }
ProviderCapabilities::BASIC,
ProviderCapabilities::FOOTPRINT, public function getCapabilities(): array
ProviderCapabilities::PICTURE, {
ProviderCapabilities::DATASHEET, return [
ProviderCapabilities::PRICE, ProviderCapabilities::BASIC,
ProviderCapabilities::PARAMETERS ProviderCapabilities::FOOTPRINT,
], ProviderCapabilities::PICTURE,
); ProviderCapabilities::DATASHEET,
ProviderCapabilities::PRICE,
];
}
public function getProviderKey(): string
{
return 'digikey';
} }
public function isActive(): bool public function isActive(): bool
@ -123,7 +128,7 @@ class DigikeyProvider implements InfoProviderInterface
} catch (\InvalidArgumentException $exception) { } catch (\InvalidArgumentException $exception) {
//Check if the exception was caused by an invalid or expired token //Check if the exception was caused by an invalid or expired token
if (str_contains($exception->getMessage(), 'access_token')) { if (str_contains($exception->getMessage(), 'access_token')) {
throw OAuthReconnectRequiredException::forProvider(self::PROVIDER_KEY); throw OAuthReconnectRequiredException::forProvider($this->getProviderKey());
} }
throw $exception; throw $exception;
@ -136,7 +141,7 @@ class DigikeyProvider implements InfoProviderInterface
foreach ($products as $product) { foreach ($products as $product) {
foreach ($product['ProductVariations'] as $variation) { foreach ($product['ProductVariations'] as $variation) {
$result[] = new SearchResultDTO( $result[] = new SearchResultDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $variation['DigiKeyProductNumber'], provider_id: $variation['DigiKeyProductNumber'],
name: $product['ManufacturerProductNumber'], name: $product['ManufacturerProductNumber'],
description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'], description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'],
@ -163,7 +168,7 @@ class DigikeyProvider implements InfoProviderInterface
} catch (\InvalidArgumentException $exception) { } catch (\InvalidArgumentException $exception) {
//Check if the exception was caused by an invalid or expired token //Check if the exception was caused by an invalid or expired token
if (str_contains($exception->getMessage(), 'access_token')) { if (str_contains($exception->getMessage(), 'access_token')) {
throw OAuthReconnectRequiredException::forProvider(self::PROVIDER_KEY); throw OAuthReconnectRequiredException::forProvider($this->getProviderKey());
} }
throw $exception; throw $exception;
@ -186,7 +191,7 @@ class DigikeyProvider implements InfoProviderInterface
} }
return new PartDetailDTO( return new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $id, provider_id: $id,
name: $product['ManufacturerProductNumber'], name: $product['ManufacturerProductNumber'],
description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'], description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'],

View file

@ -28,7 +28,6 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Settings\InfoProviderSystem\Element14Settings; use App\Settings\InfoProviderSystem\Element14Settings;
use Composer\CaBundle\CaBundle; use Composer\CaBundle\CaBundle;
@ -36,7 +35,6 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
class Element14Provider implements InfoProviderInterface, URLHandlerInfoProviderInterface class Element14Provider implements InfoProviderInterface, URLHandlerInfoProviderInterface
{ {
public const PROVIDER_KEY = 'element14';
private const ENDPOINT_URL = 'https://api.element14.com/catalog/products'; private const ENDPOINT_URL = 'https://api.element14.com/catalog/products';
private const API_VERSION_NUMBER = '1.4'; 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( return [
key: self::PROVIDER_KEY, 'name' => 'Farnell element14',
name: 'Farnell element14', 'description' => 'This provider uses the Farnell element14 API to search for parts.',
description: 'This provider uses the Farnell element14 API to search for parts.', 'url' => 'https://www.element14.com/',
url: 'https://www.element14.com/', 'disabled_help' => 'Configure the API key in the provider settings to enable.',
disabledHelp: 'Configure the API key in the provider settings to enable.', 'settings_class' => Element14Settings::class,
settingsClass: Element14Settings::class, ];
capabilities: [ }
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE, public function getProviderKey(): string
ProviderCapabilities::DATASHEET, {
ProviderCapabilities::PRICE, return 'element14';
ProviderCapabilities::PARAMETERS,
],
);
} }
public function isActive(): bool public function isActive(): bool
@ -118,7 +113,7 @@ class Element14Provider implements InfoProviderInterface, URLHandlerInfoProvider
foreach ($products as $product) { foreach ($products as $product) {
$result[] = new PartDetailDTO( $result[] = new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_id: $product['sku'], provider_key: $this->getProviderKey(), provider_id: $product['sku'],
name: $product['translatedManufacturerPartNumber'], name: $product['translatedManufacturerPartNumber'],
description: $this->displayNameToDescription($product['displayName'], $product['translatedManufacturerPartNumber']), description: $this->displayNameToDescription($product['displayName'], $product['translatedManufacturerPartNumber']),
manufacturer: $product['vendorName'] ?? $product['brandName'] ?? null, manufacturer: $product['vendorName'] ?? $product['brandName'] ?? null,
@ -306,6 +301,15 @@ class Element14Provider implements InfoProviderInterface, URLHandlerInfoProvider
return $tmp[0]; return $tmp[0];
} }
public function getCapabilities(): array
{
return [
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE,
ProviderCapabilities::DATASHEET,
];
}
public function getHandledDomains(): array public function getHandledDomains(): array
{ {
return ['element14.com', 'farnell.com', 'newark.com']; return ['element14.com', 'farnell.com', 'newark.com'];

View file

@ -25,7 +25,6 @@ namespace App\Services\InfoProviderSystem\Providers;
use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use Symfony\Component\DependencyInjection\Attribute\When; use Symfony\Component\DependencyInjection\Attribute\When;
@ -35,20 +34,19 @@ use Symfony\Component\DependencyInjection\Attribute\When;
#[When(env: 'test')] #[When(env: 'test')]
class EmptyProvider implements InfoProviderInterface class EmptyProvider implements InfoProviderInterface
{ {
public const PROVIDER_KEY = 'empty'; public function getProviderInfo(): array
public function getProviderInfo(): ProviderInfoDTO
{ {
return new ProviderInfoDTO( return [
key: self::PROVIDER_KEY, 'name' => 'Empty Provider',
name: 'Empty Provider', 'description' => 'This is a test provider',
description: 'This is a test provider', //'url' => 'https://example.com',
disabledHelp: 'This provider is disabled for testing purposes', 'disabled_help' => 'This provider is disabled for testing purposes'
capabilities: [ ];
ProviderCapabilities::BASIC, }
ProviderCapabilities::FOOTPRINT,
], public function getProviderKey(): string
); {
return 'empty';
} }
public function isActive(): bool 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 public function getDetails(string $id, array $options = []): PartDetailDTO
{ {
throw new \RuntimeException('No part details available'); throw new \RuntimeException('No part details available');

View file

@ -30,7 +30,6 @@ use App\Services\InfoProviderSystem\CreateFromUrlHelper;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Services\InfoProviderSystem\PartInfoRetriever; use App\Services\InfoProviderSystem\PartInfoRetriever;
@ -54,7 +53,6 @@ class GenericWebProvider implements InfoProviderInterface
use FixAndValidateUrlTrait; use FixAndValidateUrlTrait;
public const DISTRIBUTOR_NAME = 'Website'; public const DISTRIBUTOR_NAME = 'Website';
public const PROVIDER_KEY = 'generic_web';
private readonly HttpClientInterface $httpClient; private readonly HttpClientInterface $httpClient;
@ -71,21 +69,20 @@ class GenericWebProvider implements InfoProviderInterface
); );
} }
public function getProviderInfo(): ProviderInfoDTO public function getProviderInfo(): array
{ {
return new ProviderInfoDTO( return [
key: self::PROVIDER_KEY, 'name' => 'Generic Web URL',
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.',
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',
disabledHelp: 'Enable in settings to use this provider', 'disabled_help' => 'Enable in settings to use this provider',
settingsClass: GenericWebProviderSettings::class, 'settings_class' => GenericWebProviderSettings::class,
capabilities: [ ];
ProviderCapabilities::BASIC, }
ProviderCapabilities::PICTURE,
ProviderCapabilities::PRICE, public function getProviderKey(): string
ProviderCapabilities::GTIN, {
], return 'generic_web';
);
} }
public function isActive(): bool public function isActive(): bool
@ -230,7 +227,7 @@ class GenericWebProvider implements InfoProviderInterface
} }
return new PartDetailDTO( return new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $url, provider_id: $url,
name: $product->name?->toString() ?? $product->alternateName?->toString() ?? $product->mpn?->toString() ?? 'Unknown Name', name: $product->name?->toString() ?? $product->alternateName?->toString() ?? $product->mpn?->toString() ?? 'Unknown Name',
description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '', description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '',
@ -379,7 +376,7 @@ class GenericWebProvider implements InfoProviderInterface
)]; )];
return new PartDetailDTO( return new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $canonicalURL, provider_id: $canonicalURL,
name: $this->getMetaContent($dom, 'og:title') ?? $pageTitle, name: $this->getMetaContent($dom, 'og:title') ?? $pageTitle,
description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '', 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,
];
}
} }

View file

@ -24,7 +24,6 @@ declare(strict_types=1);
namespace App\Services\InfoProviderSystem\Providers; namespace App\Services\InfoProviderSystem\Providers;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
interface InfoProviderInterface interface InfoProviderInterface
@ -35,8 +34,26 @@ interface InfoProviderInterface
/** /**
* Get information about this provider * 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) * Checks if this provider is enabled or not (meaning that it can be used for searching)
@ -59,4 +76,12 @@ interface InfoProviderInterface
* @return PartDetailDTO * @return PartDetailDTO
*/ */
public function getDetails(string $id, array $options = []): 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;
} }

View file

@ -28,7 +28,6 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Settings\InfoProviderSystem\LCSCSettings; use App\Settings\InfoProviderSystem\LCSCSettings;
use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Cookie;
@ -40,31 +39,26 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider
private const ENDPOINT_URL = 'https://wmsc.lcsc.com/ftps/wm'; private const ENDPOINT_URL = 'https://wmsc.lcsc.com/ftps/wm';
public const DISTRIBUTOR_NAME = 'LCSC'; public const DISTRIBUTOR_NAME = 'LCSC';
public const PROVIDER_KEY = 'lcsc';
public function __construct(private readonly HttpClientInterface $lcscClient, private readonly LCSCSettings $settings) public function __construct(private readonly HttpClientInterface $lcscClient, private readonly LCSCSettings $settings)
{ {
} }
public function getProviderInfo(): ProviderInfoDTO public function getProviderInfo(): array
{ {
return new ProviderInfoDTO( return [
key: self::PROVIDER_KEY, 'name' => 'LCSC',
name: 'LCSC', 'description' => 'This provider uses the (unofficial) LCSC API to search for parts.',
description: 'This provider uses the (unofficial) LCSC API to search for parts.', 'url' => 'https://www.lcsc.com/',
url: 'https://www.lcsc.com/', 'disabled_help' => 'Enable this provider in the provider settings.',
disabledHelp: 'Enable this provider in the provider settings.', 'settings_class' => LCSCSettings::class,
settingsClass: LCSCSettings::class, ];
capabilities: [ }
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE, public function getProviderKey(): string
ProviderCapabilities::DATASHEET, {
ProviderCapabilities::PRICE, return 'lcsc';
ProviderCapabilities::FOOTPRINT,
ProviderCapabilities::PARAMETERS
],
);
} }
// This provider is always active // This provider is always active
@ -233,7 +227,7 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider
} }
return new PartDetailDTO( return new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $product['productCode'], provider_id: $product['productCode'],
name: $product['productModel'], name: $product['productModel'],
description: $this->sanitizeField($product['productIntroEn']) ?? '', description: $this->sanitizeField($product['productIntroEn']) ?? '',
@ -461,6 +455,17 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider
return $tmp[0]; return $tmp[0];
} }
public function getCapabilities(): array
{
return [
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE,
ProviderCapabilities::DATASHEET,
ProviderCapabilities::PRICE,
ProviderCapabilities::FOOTPRINT,
];
}
public function getHandledDomains(): array public function getHandledDomains(): array
{ {
return ['lcsc.com']; return ['lcsc.com'];

View file

@ -36,7 +36,6 @@ use App\Entity\Parts\ManufacturingStatus;
use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Settings\InfoProviderSystem\MouserSettings; use App\Settings\InfoProviderSystem\MouserSettings;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
@ -49,7 +48,6 @@ class MouserProvider implements InfoProviderInterface
private const ENDPOINT_URL = 'https://api.mouser.com/api/v2/search'; private const ENDPOINT_URL = 'https://api.mouser.com/api/v2/search';
public const DISTRIBUTOR_NAME = 'Mouser'; public const DISTRIBUTOR_NAME = 'Mouser';
public const PROVIDER_KEY = 'mouser';
public function __construct( public function __construct(
private readonly HttpClientInterface $mouserClient, private readonly HttpClientInterface $mouserClient,
@ -57,22 +55,20 @@ class MouserProvider implements InfoProviderInterface
) { ) {
} }
public function getProviderInfo(): ProviderInfoDTO public function getProviderInfo(): array
{ {
return new ProviderInfoDTO( return [
key: self::PROVIDER_KEY, 'name' => 'Mouser',
name: 'Mouser', 'description' => 'This provider uses the Mouser API to search for parts.',
description: 'This provider uses the Mouser API to search for parts.', 'url' => 'https://www.mouser.com/',
url: 'https://www.mouser.com/', 'disabled_help' => 'Configure the API key in the provider settings to enable.',
disabledHelp: 'Configure the API key in the provider settings to enable.', 'settings_class' => MouserSettings::class
settingsClass: MouserSettings::class, ];
capabilities: [ }
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE, public function getProviderKey(): string
ProviderCapabilities::DATASHEET, {
ProviderCapabilities::PRICE, return 'mouser';
],
);
} }
public function isActive(): bool public function isActive(): bool
@ -211,6 +207,17 @@ class MouserProvider implements InfoProviderInterface
return reset($tmp); return reset($tmp);
} }
public function getCapabilities(): array
{
return [
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE,
ProviderCapabilities::DATASHEET,
ProviderCapabilities::PRICE,
];
}
/** /**
* @param ResponseInterface $response * @param ResponseInterface $response
* @return PartDetailDTO[] * @return PartDetailDTO[]
@ -244,7 +251,7 @@ class MouserProvider implements InfoProviderInterface
$result[] = new PartDetailDTO( $result[] = new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $product['MouserPartNumber'], provider_id: $product['MouserPartNumber'],
name: $product['ManufacturerPartNumber'], name: $product['ManufacturerPartNumber'],
description: $product['Description'], description: $product['Description'],

View file

@ -88,7 +88,6 @@ use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Settings\InfoProviderSystem\OEMSecretsSettings; use App\Settings\InfoProviderSystem\OEMSecretsSettings;
use App\Settings\InfoProviderSystem\OEMSecretsSortMode; use App\Settings\InfoProviderSystem\OEMSecretsSortMode;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
@ -97,7 +96,6 @@ use Psr\Cache\CacheItemPoolInterface;
class OEMSecretsProvider implements InfoProviderInterface class OEMSecretsProvider implements InfoProviderInterface
{ {
public const PROVIDER_KEY = 'oemsecrets';
private const ENDPOINT_URL = 'https://oemsecretsapi.com/partsearch'; private const ENDPOINT_URL = 'https://oemsecretsapi.com/partsearch';
@ -229,23 +227,37 @@ class OEMSecretsProvider implements InfoProviderInterface
private array $distributorCountryCodes = []; private array $distributorCountryCodes = [];
private array $countryCodeToRegionMap = []; 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( return [
key: self::PROVIDER_KEY, 'name' => 'OEMSecrets',
name: 'OEMSecrets', 'description' => 'This provider uses the OEMSecrets API to search for parts.',
description: 'This provider uses the OEMSecrets API to search for parts.', 'url' => 'https://www.oemsecrets.com/',
url: 'https://www.oemsecrets.com/', 'disabled_help' => 'Configure the API key in the provider settings to enable.',
disabledHelp: 'Configure the API key in the provider settings to enable.', 'settings_class' => OEMSecretsSettings::class
settingsClass: OEMSecretsSettings::class, ];
capabilities: [ }
ProviderCapabilities::BASIC, /**
ProviderCapabilities::PICTURE, * Returns a unique key for this provider, which will be saved into the database
ProviderCapabilities::DATASHEET, * and used to identify the provider
ProviderCapabilities::PRICE, * @return string A unique key for this provider (e.g. "digikey")
ProviderCapabilities::PARAMETERS */
], 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. * Currently, this list is purely informational and not used in functional checks.
* @return ProviderCapabilities[] * @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, * 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`. * 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 there is no existing basic info array, we create a new one
if (is_null($existingBasicInfo)) { if (is_null($existingBasicInfo)) {
return [ return [
'provider_key' => self::PROVIDER_KEY, 'provider_key' => $this->getProviderKey(),
'provider_id' => $provider_id, 'provider_id' => $provider_id,
'name' => $product['part_number'], 'name' => $product['part_number'],
'description' => $description, 'description' => $description,
@ -893,7 +916,7 @@ class OEMSecretsProvider implements InfoProviderInterface
// Update fields only if empty or undefined, with additional check for preview_image_url // Update fields only if empty or undefined, with additional check for preview_image_url
return [ return [
'provider_key' => $existingBasicInfo['provider_key'] ?? self::PROVIDER_KEY, 'provider_key' => $existingBasicInfo['provider_key'] ?? $this->getProviderKey(),
'provider_id' => $existingBasicInfo['provider_id'] ?? $provider_id, 'provider_id' => $existingBasicInfo['provider_id'] ?? $provider_id,
'name' => $existingBasicInfo['name'] ?? $product['part_number'], 'name' => $existingBasicInfo['name'] ?? $product['part_number'],
// Update description if it's null/empty // Update description if it's null/empty
@ -1247,7 +1270,7 @@ class OEMSecretsProvider implements InfoProviderInterface
*/ */
private function generateInquiryUrl(string $partNumber, string $oemInquiry = 'compare/'): string private function generateInquiryUrl(string $partNumber, string $oemInquiry = 'compare/'): string
{ {
$baseUrl = rtrim($this->getProviderInfo()->url, '/') . '/'; $baseUrl = rtrim($this->getProviderInfo()['url'], '/') . '/';
$inquiryPath = trim($oemInquiry, '/') . '/'; $inquiryPath = trim($oemInquiry, '/') . '/';
$encodedPartNumber = urlencode(trim($partNumber)); $encodedPartNumber = urlencode(trim($partNumber));
return $baseUrl . $inquiryPath . $encodedPartNumber; return $baseUrl . $inquiryPath . $encodedPartNumber;

View file

@ -28,7 +28,6 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\OAuth\OAuthTokenManager; use App\Services\OAuth\OAuthTokenManager;
use App\Settings\InfoProviderSystem\OctopartSettings; use App\Settings\InfoProviderSystem\OctopartSettings;
@ -44,8 +43,6 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*/ */
class OctopartProvider implements InfoProviderInterface class OctopartProvider implements InfoProviderInterface
{ {
public const PROVIDER_KEY = 'octopart';
private const OAUTH_APP_NAME = 'ip_octopart_oauth'; private const OAUTH_APP_NAME = 'ip_octopart_oauth';
/** /**
@ -167,25 +164,20 @@ class OctopartProvider implements InfoProviderInterface
return $response->toArray(true); return $response->toArray(true);
} }
public function getProviderInfo(): ProviderInfoDTO public function getProviderInfo(): array
{ {
return new ProviderInfoDTO( return [
key: self::PROVIDER_KEY, 'name' => 'Octopart',
name: 'Octopart', 'description' => 'This provider uses the Nexar/Octopart API to search for parts on Octopart.',
description: 'This provider uses the Nexar/Octopart API to search for parts on Octopart.', 'url' => 'https://www.octopart.com/',
url: 'https://www.octopart.com/', 'disabled_help' => 'Set the Client ID and Secret in provider settings.',
disabledHelp: 'Set the Client ID and Secret in provider settings.', 'settings_class' => OctopartSettings::class
settingsClass: OctopartSettings::class, ];
capabilities: [ }
ProviderCapabilities::BASIC,
ProviderCapabilities::FOOTPRINT, public function getProviderKey(): string
ProviderCapabilities::PICTURE, {
ProviderCapabilities::DATASHEET, return 'octopart';
ProviderCapabilities::PRICE,
ProviderCapabilities::PARAMETERS
],
expensive: true,
);
} }
public function isActive(): bool public function isActive(): bool
@ -315,7 +307,7 @@ class OctopartProvider implements InfoProviderInterface
} }
return new PartDetailDTO( return new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $part['id'], provider_id: $part['id'],
name: $part['mpn'], name: $part['mpn'],
description: $part['shortDescription'] ?? null, description: $part['shortDescription'] ?? null,
@ -405,4 +397,14 @@ class OctopartProvider implements InfoProviderInterface
return $tmp; return $tmp;
} }
public function getCapabilities(): array
{
return [
ProviderCapabilities::BASIC,
ProviderCapabilities::FOOTPRINT,
ProviderCapabilities::PICTURE,
ProviderCapabilities::DATASHEET,
ProviderCapabilities::PRICE,
];
}
} }

View file

@ -29,7 +29,6 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\PollinSettings; use App\Settings\InfoProviderSystem\PollinSettings;
@ -39,7 +38,6 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInterface class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInterface
{ {
public const PROVIDER_KEY = 'pollin';
public function __construct(private readonly HttpClientInterface $client, public function __construct(private readonly HttpClientInterface $client,
private readonly PollinSettings $settings, private readonly PollinSettings $settings,
@ -47,23 +45,20 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt
{ {
} }
public function getProviderInfo(): ProviderInfoDTO public function getProviderInfo(): array
{ {
return new ProviderInfoDTO( return [
key: self::PROVIDER_KEY, 'name' => 'Pollin',
name: 'Pollin', 'description' => 'Webscraping from pollin.de to get part information',
description: 'Webscraping from pollin.de to get part information', 'url' => 'https://www.pollin.de/',
url: 'https://www.pollin.de/', 'disabled_help' => 'Enable the provider in provider settings',
disabledHelp: 'Enable the provider in provider settings', 'settings_class' => PollinSettings::class,
settingsClass: PollinSettings::class, ];
capabilities: [ }
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE, public function getProviderKey(): string
ProviderCapabilities::PRICE, {
ProviderCapabilities::DATASHEET, return 'pollin';
ProviderCapabilities::PARAMETERS
],
);
} }
public function isActive(): bool public function isActive(): bool
@ -93,7 +88,7 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt
//Iterate over each div.product-box //Iterate over each div.product-box
$dom->filter('div.product-box')->each(function (Crawler $node) use (&$results) { $dom->filter('div.product-box')->each(function (Crawler $node) use (&$results) {
$results[] = new SearchResultDTO( $results[] = new SearchResultDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $node->filter('meta[itemprop="productID"]')->attr('content'), provider_id: $node->filter('meta[itemprop="productID"]')->attr('content'),
name: $node->filter('a.product-name')->text(), name: $node->filter('a.product-name')->text(),
description: '', description: '',
@ -161,7 +156,7 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt
$purchaseInfo = new PurchaseInfoDTO('Pollin', $orderId, $this->parsePrices($dom), $productPageUrl); $purchaseInfo = new PurchaseInfoDTO('Pollin', $orderId, $this->parsePrices($dom), $productPageUrl);
return new PartDetailDTO( return new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $orderId, provider_id: $orderId,
name: trim($dom->filter('meta[property="og:title"]')->attr('content')), name: trim($dom->filter('meta[property="og:title"]')->attr('content')),
description: $dom->filter('meta[property="og:description"]')->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 public function getHandledDomains(): array
{ {
return ['pollin.de']; return ['pollin.de'];

View file

@ -26,28 +26,28 @@ namespace App\Services\InfoProviderSystem\Providers;
/** /**
* This enum contains all capabilities (which data it can provide) a provider can have. * 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 */ /** 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 */ /** Provider can provide a picture for a part */
case PICTURE = 'PICTURE'; case PICTURE;
/** Provider can provide datasheets for a part */ /** Provider can provide datasheets for a part */
case DATASHEET = 'DATASHEET'; case DATASHEET;
/** Provider can provide prices for a part */ /** Provider can provide prices for a part */
case PRICE = 'PRICE'; case PRICE;
/** Information about the footprint of a part */ /** Information about the footprint of a part */
case FOOTPRINT = 'FOOTPRINT'; case FOOTPRINT;
/** Provider can provide GTIN for a part */ /** Provider can provide GTIN for a part */
case GTIN = 'GTIN'; case GTIN;
/** Provider can provide parameters/specifications for a part */ /** Provider can provide parameters/specifications for a part */
case PARAMETERS = 'PARAMETERS'; case PARAMETERS;
/** /**
* Get the order index for displaying capabilities in a stable order. * Get the order index for displaying capabilities in a stable order.

View file

@ -28,7 +28,6 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\ReicheltSettings; use App\Settings\InfoProviderSystem\ReicheltSettings;
@ -39,7 +38,6 @@ class ReicheltProvider implements InfoProviderInterface
{ {
public const DISTRIBUTOR_NAME = "Reichelt"; public const DISTRIBUTOR_NAME = "Reichelt";
public const PROVIDER_KEY = 'reichelt';
private readonly HttpClientInterface $client; private readonly HttpClientInterface $client;
@ -50,24 +48,20 @@ class ReicheltProvider implements InfoProviderInterface
$this->client = new RandomizeUseragentHttpClient($client); $this->client = new RandomizeUseragentHttpClient($client);
} }
public function getProviderInfo(): ProviderInfoDTO public function getProviderInfo(): array
{ {
return new ProviderInfoDTO( return [
key: self::PROVIDER_KEY, 'name' => 'Reichelt',
name: 'Reichelt', 'description' => 'Webscraping from reichelt.com to get part information',
description: 'Webscraping from reichelt.com to get part information', 'url' => 'https://www.reichelt.com/',
url: 'https://www.reichelt.com/', 'disabled_help' => 'Enable provider in provider settings.',
disabledHelp: 'Enable provider in provider settings.', 'settings_class' => ReicheltSettings::class,
settingsClass: ReicheltSettings::class, ];
capabilities: [ }
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE, public function getProviderKey(): string
ProviderCapabilities::DATASHEET, {
ProviderCapabilities::PRICE, return 'reichelt';
ProviderCapabilities::GTIN,
ProviderCapabilities::PARAMETERS
],
);
} }
public function isActive(): bool public function isActive(): bool
@ -99,7 +93,7 @@ class ReicheltProvider implements InfoProviderInterface
$pictureURL = $element->filter("div.al_artlogo img")->attr('src'); $pictureURL = $element->filter("div.al_artlogo img")->attr('src');
$results[] = new SearchResultDTO( $results[] = new SearchResultDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $artId, provider_id: $artId,
name: $productID, name: $productID,
description: $name, description: $name,
@ -179,7 +173,7 @@ class ReicheltProvider implements InfoProviderInterface
//Create part object //Create part object
return new PartDetailDTO( return new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $id, provider_id: $id,
name: $json[0]['article_artnr'], name: $json[0]['article_artnr'],
description: $json[0]['article_besch'], 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); 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,
];
}
} }

View file

@ -28,14 +28,12 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\TMESettings; use App\Settings\InfoProviderSystem\TMESettings;
class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterface class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterface
{ {
public const PROVIDER_KEY = 'tme';
private const VENDOR_NAME = '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( return [
key: self::PROVIDER_KEY, 'name' => 'TME',
name: 'TME', 'description' => 'This provider uses the API of TME (Transfer Multipart).',
description: 'This provider uses the API of TME (Transfer Multipart).', 'url' => 'https://tme.eu/',
url: 'https://tme.eu/', 'disabled_help' => 'Configure the API Token and secret in provider settings to use this provider.',
disabledHelp: 'Configure the API Token and secret in provider settings to use this provider.', 'settings_class' => TMESettings::class
settingsClass: TMESettings::class, ];
capabilities: [ }
ProviderCapabilities::BASIC,
ProviderCapabilities::FOOTPRINT, public function getProviderKey(): string
ProviderCapabilities::PICTURE, {
ProviderCapabilities::DATASHEET, return 'tme';
ProviderCapabilities::PRICE,
ProviderCapabilities::PARAMETERS
],
);
} }
public function isActive(): bool public function isActive(): bool
@ -89,7 +83,7 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
foreach($data['ProductList'] as $product) { foreach($data['ProductList'] as $product) {
$result[] = new SearchResultDTO( $result[] = new SearchResultDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $product['Symbol'], provider_id: $product['Symbol'],
name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'], name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'],
description: $product['Description'], description: $product['Description'],
@ -125,7 +119,7 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
$parameters = $this->getParameters($id, $footprint); $parameters = $this->getParameters($id, $footprint);
return new PartDetailDTO( return new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $product['Symbol'], provider_id: $product['Symbol'],
name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'], name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'],
description: $product['Description'], description: $product['Description'],
@ -296,6 +290,17 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
return $url; return $url;
} }
public function getCapabilities(): array
{
return [
ProviderCapabilities::BASIC,
ProviderCapabilities::FOOTPRINT,
ProviderCapabilities::PICTURE,
ProviderCapabilities::DATASHEET,
ProviderCapabilities::PRICE,
];
}
public function getHandledDomains(): array public function getHandledDomains(): array
{ {
return ['tme.eu']; return ['tme.eu'];

View file

@ -25,7 +25,6 @@ namespace App\Services\InfoProviderSystem\Providers;
use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use Symfony\Component\DependencyInjection\Attribute\When; use Symfony\Component\DependencyInjection\Attribute\When;
@ -35,20 +34,20 @@ use Symfony\Component\DependencyInjection\Attribute\When;
#[When(env: 'test')] #[When(env: 'test')]
class TestProvider implements InfoProviderInterface class TestProvider implements InfoProviderInterface
{ {
public const PROVIDER_KEY = 'test';
public function getProviderInfo(): ProviderInfoDTO public function getProviderInfo(): array
{ {
return new ProviderInfoDTO( return [
key: self::PROVIDER_KEY, 'name' => 'Test Provider',
name: 'Test Provider', 'description' => 'This is a test provider',
description: 'This is a test provider', //'url' => 'https://example.com',
disabledHelp: 'This provider is disabled for testing purposes', 'disabled_help' => 'This provider is disabled for testing purposes'
capabilities: [ ];
ProviderCapabilities::BASIC, }
ProviderCapabilities::FOOTPRINT,
], public function getProviderKey(): string
); {
return 'test';
} }
public function isActive(): bool public function isActive(): bool
@ -59,16 +58,24 @@ class TestProvider implements InfoProviderInterface
public function searchByKeyword(string $keyword, array $options = []): array public function searchByKeyword(string $keyword, array $options = []): array
{ {
return [ return [
new SearchResultDTO(provider_key: self::PROVIDER_KEY, provider_id: 'element1', name: 'Element 1', description: 'fd'), new SearchResultDTO(provider_key: $this->getProviderKey(), 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: $this->getProviderKey(), 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: 'element3', name: 'Element 3', description: 'fd'),
];
}
public function getCapabilities(): array
{
return [
ProviderCapabilities::BASIC,
ProviderCapabilities::FOOTPRINT,
]; ];
} }
public function getDetails(string $id, array $options = []): PartDetailDTO public function getDetails(string $id, array $options = []): PartDetailDTO
{ {
return new PartDetailDTO( return new PartDetailDTO(
provider_key: self::PROVIDER_KEY, provider_key: $this->getProviderKey(),
provider_id: $id, provider_id: $id,
name: 'Test Element', name: 'Test Element',
description: 'fd', description: 'fd',

View file

@ -25,7 +25,7 @@ namespace App\State\Mcp;
use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface; use ApiPlatform\State\ProviderInterface;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO; use App\Services\InfoProviderSystem\DTOs\InfoProviderDTO;
use App\Services\InfoProviderSystem\ProviderRegistry; 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 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 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 private function listActiveProviders(): array
{ {
$result = []; $result = [];
foreach ($this->providerRegistry->getActiveProviders() as $provider) { 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; return $result;

View file

@ -59,7 +59,7 @@ final readonly class InfoProviderExtension
public function getInfoProviderName(string $key): ?string public function getInfoProviderName(string $key): ?string
{ {
try { try {
return $this->providerRegistry->getProviderByKey($key)->getProviderInfo()->name; return $this->providerRegistry->getProviderByKey($key)->getProviderInfo()['name'];
} catch (\InvalidArgumentException) { } catch (\InvalidArgumentException) {
return null; return null;
} }

View file

@ -8,41 +8,35 @@
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<h5> <h5>
{% if provider.providerInfo.url is not empty %} {% if provider.providerInfo.url is defined and provider.providerInfo.url is not empty %}
<a href="{{ provider.providerInfo.url }}" target="_blank">{{ provider.providerInfo.name }}</a> <a href="{{ provider.providerInfo.url }}" target="_blank">{{ provider.providerInfo.name }}</a>
{% else %} {% else %}
{{ provider.providerInfo.name }} {{ provider.providerInfo.name | trans }}
{% endif %} {% endif %}
</h5> </h5>
<div> <div>
{% if provider.providerInfo.description is not null %} {% if provider.providerInfo.description is defined and provider.providerInfo.description is not null %}
{{ provider.providerInfo.description }} {{ provider.providerInfo.description | trans }}
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="col-6"> <div class="col-6">
{% if provider.providerInfo.settingsClass is not null %} {% if provider.providerInfo.settings_class is defined %}
<a href="{{ path('info_providers_provider_settings', {'provider': provider.providerInfo.key}) }}" class="btn btn-primary btn-sm {% if not is_granted('@config.change_system_settings') %}disabled{% endif %}" <a href="{{ path('info_providers_provider_settings', {'provider': provider.providerKey}) }}" class="btn btn-primary btn-sm {% if not is_granted('@config.change_system_settings') %}disabled{% endif %}"
title="{% trans %}info_providers.settings.title{% endtrans %}" title="{% trans %}info_providers.settings.title{% endtrans %}"
><i class="fa-solid fa-cog"></i></a> ><i class="fa-solid fa-cog"></i></a>
{% endif %} {% endif %}
{% if provider.providerInfo.expensive %} {% for capability in provider.capabilities|sort((a, b) => a.orderIndex <=> b.orderIndex) %}
<span class="badge bg-warning" title="{% trans %}info_providers.expensive.tooltip{% endtrans %}">
<i class="fa-fw fa-solid fa-euro-sign"></i>
{% trans %}info_providers.expensive{% endtrans %}
</span>
{% endif %}
{% for capability in provider.providerInfo.capabilities|sort((a, b) => a.orderIndex <=> b.orderIndex) %}
{# @var capability \App\Services\InfoProviderSystem\Providers\ProviderCapabilities #} {# @var capability \App\Services\InfoProviderSystem\Providers\ProviderCapabilities #}
<span class="badge text-bg-secondary"> <span class="badge text-bg-secondary">
<i class="{{ capability.fAIconClass }} fa-fw"></i> <i class="{{ capability.fAIconClass }} fa-fw"></i>
{{ capability.translationKey|trans }} {{ capability.translationKey|trans }}
</span> </span>
{% endfor %} {% 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 %}
<br> <br>
<a href="{{ path('oauth_client_connect', {'name': provider.providerInfo.oauthAppName}) }}" target="_blank" class="btn btn-outline-secondary btn-sm mt-2">{% trans %}oauth_client.connect.btn{% endtrans %}</a> <a href="{{ path('oauth_client_connect', {'name': provider.providerInfo.oauth_app_name}) }}" target="_blank" class="btn btn-outline-secondary btn-sm mt-2">{% trans %}oauth_client.connect.btn{% endtrans %}</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -50,9 +44,9 @@
<div class="row"> <div class="row">
<div class="col text-danger"> <div class="col text-danger">
<i class="fa-solid fa-circle-exclamation"></i> <b>{% trans %}info_providers.providers_list.disabled{% endtrans %}</b> <i class="fa-solid fa-circle-exclamation"></i> <b>{% trans %}info_providers.providers_list.disabled{% endtrans %}</b>
{% if provider.providerInfo.disabledHelp is not empty %} {% if provider.providerInfo.disabled_help is defined and provider.providerInfo.disabled_help is not empty %}
<br> <br>
<span class="text-muted">{{ provider.providerInfo.disabledHelp }}</span> <span class="text-muted">{{ provider.providerInfo.disabled_help|trans }}</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View file

@ -10,7 +10,7 @@
{% block card_content %} {% block card_content %}
<div class="{{ offset_label }}"> <div class="{{ offset_label }}">
<h3> <h3>
{% if info_provider_info.url is not empty %} {% if info_provider_info.url is defined %}
<a href="{{ info_provider_info.url }}" class="link-external" target="_blank" rel="nofollow">{{ info_provider_info.name }}</a> <a href="{{ info_provider_info.url }}" class="link-external" target="_blank" rel="nofollow">{{ info_provider_info.name }}</a>
{% else %} {% else %}
{{ info_provider_info.name }} {{ info_provider_info.name }}

View file

@ -39,16 +39,6 @@ class InfoProviderEndpointTest extends AuthenticatedApiTestCase
$keys = array_column($json['hydra:member'], 'key'); $keys = array_column($json['hydra:member'], 'key');
self::assertContains('test', $keys); 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 public function testListInfoProvidersRequiresAuthentication(): void

View file

@ -24,7 +24,6 @@ namespace App\Tests\Services\InfoProviderSystem\DTOs;
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO; use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
final class BulkSearchFieldMappingDTOTest extends TestCase final class BulkSearchFieldMappingDTOTest extends TestCase
@ -33,7 +32,7 @@ final class BulkSearchFieldMappingDTOTest extends TestCase
public function testProviderInstanceNormalization(): void public function testProviderInstanceNormalization(): void
{ {
$mockProvider = $this->createMock(InfoProviderInterface::class); $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); $fieldMapping = new BulkSearchFieldMappingDTO(field: 'mpn', providers: ['provider1', $mockProvider], priority: 5);
$this->assertSame(['provider1', 'mock_provider'], $fieldMapping->providers); $this->assertSame(['provider1', 'mock_provider'], $fieldMapping->providers);

View file

@ -22,7 +22,6 @@ declare(strict_types=1);
*/ */
namespace App\Tests\Services\InfoProviderSystem; namespace App\Tests\Services\InfoProviderSystem;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\ProviderRegistry; use App\Services\InfoProviderSystem\ProviderRegistry;
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
use App\Services\InfoProviderSystem\Providers\URLHandlerInfoProviderInterface; use App\Services\InfoProviderSystem\Providers\URLHandlerInfoProviderInterface;
@ -47,7 +46,7 @@ final class ProviderRegistryTest extends TestCase
public function getMockProvider(string $key, bool $active = true): InfoProviderInterface public function getMockProvider(string $key, bool $active = true): InfoProviderInterface
{ {
$mock = $this->createMockForIntersectionOfInterfaces([InfoProviderInterface::class, URLHandlerInfoProviderInterface::class]); $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('isActive')->willReturn($active);
$mock->method('getHandledDomains')->willReturn(["$key.com", "test.$key.de"]); $mock->method('getHandledDomains')->willReturn(["$key.com", "test.$key.de"]);

View file

@ -27,7 +27,6 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\Providers\LCSCProvider; use App\Services\InfoProviderSystem\Providers\LCSCProvider;
use App\Services\InfoProviderSystem\Providers\ProviderCapabilities; use App\Services\InfoProviderSystem\Providers\ProviderCapabilities;
@ -57,10 +56,18 @@ final class LCSCProviderTest extends TestCase
{ {
$info = $this->provider->getProviderInfo(); $info = $this->provider->getProviderInfo();
$this->assertInstanceOf(ProviderInfoDTO::class, $info); $this->assertIsArray($info);
$this->assertSame('lcsc', $info->key); $this->assertArrayHasKey('name', $info);
$this->assertEquals('LCSC', $info->name); $this->assertArrayHasKey('description', $info);
$this->assertEquals('https://www.lcsc.com/', $info->url); $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 public function testIsActiveWhenEnabled(): void
@ -79,7 +86,7 @@ final class LCSCProviderTest extends TestCase
public function testGetCapabilities(): void public function testGetCapabilities(): void
{ {
$capabilities = $this->provider->getProviderInfo()->capabilities; $capabilities = $this->provider->getCapabilities();
$this->assertIsArray($capabilities); $this->assertIsArray($capabilities);
$this->assertContains(ProviderCapabilities::BASIC, $capabilities); $this->assertContains(ProviderCapabilities::BASIC, $capabilities);

View file

@ -24,7 +24,6 @@ namespace App\Tests\Services\InfoProviderSystem\Providers;
use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\ManufacturingStatus;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Services\InfoProviderSystem\Providers\ProviderCapabilities; use App\Services\InfoProviderSystem\Providers\ProviderCapabilities;
@ -213,10 +212,17 @@ final class TMEProviderTest extends TestCase
{ {
$info = $this->provider->getProviderInfo(); $info = $this->provider->getProviderInfo();
$this->assertInstanceOf(ProviderInfoDTO::class, $info); $this->assertIsArray($info);
$this->assertSame('tme', $info->key); $this->assertArrayHasKey('name', $info);
$this->assertEquals('TME', $info->name); $this->assertArrayHasKey('description', $info);
$this->assertEquals('https://tme.eu/', $info->url); $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 public function testIsActiveWithCredentials(): void
@ -233,7 +239,7 @@ final class TMEProviderTest extends TestCase
public function testGetCapabilities(): void public function testGetCapabilities(): void
{ {
$capabilities = $this->provider->getProviderInfo()->capabilities; $capabilities = $this->provider->getCapabilities();
$this->assertIsArray($capabilities); $this->assertIsArray($capabilities);
$this->assertContains(ProviderCapabilities::BASIC, $capabilities); $this->assertContains(ProviderCapabilities::BASIC, $capabilities);

View file

@ -26,7 +26,6 @@ namespace App\Tests\State\Mcp;
use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Get;
use App\Mcp\DTO\InfoProviderPartDetailsInput; use App\Mcp\DTO\InfoProviderPartDetailsInput;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOtoEntityConverter; use App\Services\InfoProviderSystem\DTOtoEntityConverter;
use App\Services\InfoProviderSystem\PartInfoRetriever; use App\Services\InfoProviderSystem\PartInfoRetriever;
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
@ -46,14 +45,14 @@ final class GetInfoProviderPartDetailsProcessorTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
$activeProvider = $this->createMock(InfoProviderInterface::class); $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('isActive')->willReturn(true);
$activeProvider->method('getDetails')->willReturn( $activeProvider->method('getDetails')->willReturn(
new PartDetailDTO(provider_key: 'test1', provider_id: '42', name: 'Element 42', description: 'desc') new PartDetailDTO(provider_key: 'test1', provider_id: '42', name: 'Element 42', description: 'desc')
); );
$inactiveProvider = $this->createMock(InfoProviderInterface::class); $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); $inactiveProvider->method('isActive')->willReturn(false);
$providerRegistry = new ProviderRegistry([$activeProvider, $inactiveProvider]); $providerRegistry = new ProviderRegistry([$activeProvider, $inactiveProvider]);

View file

@ -25,7 +25,7 @@ namespace App\Tests\State\Mcp;
use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Get;
use App\Mcp\DTO\ListInfoProvidersInput; 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\InfoProviderInterface;
use App\Services\InfoProviderSystem\Providers\ProviderCapabilities; use App\Services\InfoProviderSystem\Providers\ProviderCapabilities;
use App\Services\InfoProviderSystem\ProviderRegistry; use App\Services\InfoProviderSystem\ProviderRegistry;
@ -37,18 +37,20 @@ final class ListInfoProvidersProcessorTest extends TestCase
public function testOnlyActiveProvidersAreListed(): void public function testOnlyActiveProvidersAreListed(): void
{ {
$active = $this->createMock(InfoProviderInterface::class); $active = $this->createMock(InfoProviderInterface::class);
$active->method('getProviderKey')->willReturn('active_provider');
$active->method('isActive')->willReturn(true); $active->method('isActive')->willReturn(true);
$active->method('getProviderInfo')->willReturn(new ProviderInfoDTO( $active->method('getProviderInfo')->willReturn([
key: 'active_provider', 'name' => 'Active Provider',
name: 'Active Provider', 'description' => 'A provider that is active',
description: 'A provider that is active', 'url' => 'https://example.com',
url: 'https://example.com', ]);
capabilities: [ProviderCapabilities::BASIC, ProviderCapabilities::PRICE], $active->method('getCapabilities')->willReturn([ProviderCapabilities::BASIC, ProviderCapabilities::PRICE]);
));
$disabled = $this->createMock(InfoProviderInterface::class); $disabled = $this->createMock(InfoProviderInterface::class);
$disabled->method('getProviderKey')->willReturn('disabled_provider');
$disabled->method('isActive')->willReturn(false); $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]); $registry = new ProviderRegistry([$active, $disabled]);
$processor = new ListInfoProvidersProcessor($registry); $processor = new ListInfoProvidersProcessor($registry);
@ -56,11 +58,11 @@ final class ListInfoProvidersProcessorTest extends TestCase
$result = $processor->process(new ListInfoProvidersInput(), new Get()); $result = $processor->process(new ListInfoProvidersInput(), new Get());
$this->assertCount(1, $result); $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]->key);
$this->assertSame('Active Provider', $result[0]->name); $this->assertSame('Active Provider', $result[0]->name);
$this->assertSame('A provider that is active', $result[0]->description); $this->assertSame('A provider that is active', $result[0]->description);
$this->assertSame('https://example.com', $result[0]->url); $this->assertSame('https://example.com', $result[0]->url);
$this->assertSame([ProviderCapabilities::BASIC, ProviderCapabilities::PRICE], $result[0]->capabilities); $this->assertSame(['BASIC', 'PRICE'], $result[0]->capabilities);
} }
} }

View file

@ -25,7 +25,6 @@ namespace App\Tests\State\Mcp;
use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Get;
use App\Mcp\DTO\InfoProviderSearchInput; use App\Mcp\DTO\InfoProviderSearchInput;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Services\InfoProviderSystem\DTOtoEntityConverter; use App\Services\InfoProviderSystem\DTOtoEntityConverter;
use App\Services\InfoProviderSystem\PartInfoRetriever; use App\Services\InfoProviderSystem\PartInfoRetriever;
@ -70,8 +69,8 @@ final class SearchInfoProvidersProcessorTest extends TestCase
private function getMockProvider(string $key, bool $active = true): InfoProviderInterface private function getMockProvider(string $key, bool $active = true): InfoProviderInterface
{ {
$mock = $this->createMock(InfoProviderInterface::class); $mock = $this->createMock(InfoProviderInterface::class);
$mock->method('getProviderKey')->willReturn($key);
$mock->method('isActive')->willReturn($active); $mock->method('isActive')->willReturn($active);
$mock->method('getProviderInfo')->willReturn(new ProviderInfoDTO(key: $key, name: $key));
$mock->method('searchByKeyword')->willReturn([ $mock->method('searchByKeyword')->willReturn([
new SearchResultDTO(provider_key: $key, provider_id: '1', name: 'Element 1', description: 'desc'), new SearchResultDTO(provider_key: $key, provider_id: '1', name: 'Element 1', description: 'desc'),
]); ]);

View file

@ -13805,23 +13805,5 @@ Buerklin-API Authentication server:
<target>MCP endpoint</target> <target>MCP endpoint</target>
</segment> </segment>
</unit> </unit>
<unit id="a1wK4h_" name="info_providers.capabilities.parameters">
<segment>
<source>info_providers.capabilities.parameters</source>
<target>Parameters</target>
</segment>
</unit>
<unit id="QivC_me" name="info_providers.expensive">
<segment>
<source>info_providers.expensive</source>
<target>Expensive</target>
</segment>
</unit>
<unit id="Hiw406i" name="info_providers.expensive.tooltip">
<segment>
<source>info_providers.expensive.tooltip</source>
<target>Using this info provider can cause additional costs, or has very strict rate limits.</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>