Compare commits

..

5 commits

Author SHA1 Message Date
Jan Böhmer
51ce453d1a Removed MCP Schema Factory decorator as fix was merge and the upstream version actually contained more fixes
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
2026-07-26 18:45:50 +02:00
Jan Böhmer
ef690fe37f Hint the parameter capabilities for more info providers 2026-07-26 14:16:47 +02:00
Jan Böhmer
6a4d816545 Show the expensive tag in info providers list 2026-07-26 14:12:49 +02:00
Jan Böhmer
73941707b5 Mark some info providers as expensive, to hint that to AI providers 2026-07-26 14:07:23 +02:00
Jan Böhmer
071ffe322d Reworked info provider system to merge all provider metadata into a unified DTO 2026-07-26 13:55:27 +02:00
42 changed files with 525 additions and 763 deletions

View file

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

View file

@ -88,7 +88,7 @@ class InfoProviderController extends AbstractController
$this->denyAccessUnlessGranted('@info_providers.create_parts');
$providerInstance = $this->providerRegistry->getProviderByKey($provider);
$settingsClass = $providerInstance->getProviderInfo()['settings_class'] ?? throw new \LogicException('Provider ' . $provider . ' does not have a settings class defined');
$settingsClass = $providerInstance->getProviderInfo()->settingsClass ?? throw new \LogicException('Provider ' . $provider . ' does not have a settings class defined');
//Create a clone of the settings object
$settings = $this->settingsManager->createTemporaryCopy($settingsClass);

View file

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

View file

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

View file

@ -1,162 +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\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);
if ($provider !== null && $provider->isActive() && $provider->getProviderKey() !== $callingInfoProvider->getProviderKey()) {
if ($provider !== null && $provider->isActive() && $provider->getProviderInfo()->key !== $callingInfoProvider->getProviderInfo()->key) {
try {
$id = $provider->getIDFromURL($url);
if ($id !== null) {

View file

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

View file

@ -1,73 +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\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

@ -0,0 +1,104 @@
<?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
$options_hash = hash('xxh3', json_encode($options_without_cache, JSON_THROW_ON_ERROR));
$cache_key = "search_{$provider->getProviderKey()}_{$escaped_keyword}_{$options_hash}";
$cache_key = "search_{$provider->getProviderInfo()->key}_{$escaped_keyword}_{$options_hash}";
//If no_cache is set, bypass the cache and get fresh results from the provider
if ($no_cache) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,6 +30,7 @@ use App\Services\InfoProviderSystem\CreateFromUrlHelper;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Services\InfoProviderSystem\PartInfoRetriever;
@ -53,6 +54,7 @@ class GenericWebProvider implements InfoProviderInterface
use FixAndValidateUrlTrait;
public const DISTRIBUTOR_NAME = 'Website';
public const PROVIDER_KEY = 'generic_web';
private readonly HttpClientInterface $httpClient;
@ -69,20 +71,21 @@ class GenericWebProvider implements InfoProviderInterface
);
}
public function getProviderInfo(): array
public function getProviderInfo(): ProviderInfoDTO
{
return [
'name' => 'Generic Web URL',
'description' => 'Tries to extract a part from a given product webpage URL using common metadata standards like JSON-LD and OpenGraph.',
//'url' => 'https://example.com',
'disabled_help' => 'Enable in settings to use this provider',
'settings_class' => GenericWebProviderSettings::class,
];
}
public function getProviderKey(): string
{
return 'generic_web';
return new ProviderInfoDTO(
key: self::PROVIDER_KEY,
name: 'Generic Web URL',
description: 'Tries to extract a part from a given product webpage URL using common metadata standards like JSON-LD and OpenGraph.',
disabledHelp: 'Enable in settings to use this provider',
settingsClass: GenericWebProviderSettings::class,
capabilities: [
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE,
ProviderCapabilities::PRICE,
ProviderCapabilities::GTIN,
],
);
}
public function isActive(): bool
@ -227,7 +230,7 @@ class GenericWebProvider implements InfoProviderInterface
}
return new PartDetailDTO(
provider_key: $this->getProviderKey(),
provider_key: self::PROVIDER_KEY,
provider_id: $url,
name: $product->name?->toString() ?? $product->alternateName?->toString() ?? $product->mpn?->toString() ?? 'Unknown Name',
description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '',
@ -376,7 +379,7 @@ class GenericWebProvider implements InfoProviderInterface
)];
return new PartDetailDTO(
provider_key: $this->getProviderKey(),
provider_key: self::PROVIDER_KEY,
provider_id: $canonicalURL,
name: $this->getMetaContent($dom, 'og:title') ?? $pageTitle,
description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '',
@ -387,13 +390,4 @@ class GenericWebProvider implements InfoProviderInterface
);
}
public function getCapabilities(): array
{
return [
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE,
ProviderCapabilities::PRICE,
ProviderCapabilities::GTIN,
];
}
}

View file

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace App\Services\InfoProviderSystem\Providers;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
interface InfoProviderInterface
@ -34,26 +35,8 @@ interface InfoProviderInterface
/**
* Get information about this provider
*
* @return array An associative array with the following keys (? means optional):
* - name: The (user friendly) name of the provider (e.g. "Digikey"), will be translated
* - description?: A short description of the provider (e.g. "Digikey is a ..."), will be translated
* - logo?: The logo of the provider (e.g. "digikey.png")
* - url?: The url of the provider (e.g. "https://www.digikey.com")
* - disabled_help?: A help text which is shown when the provider is disabled, explaining how to enable it
* - oauth_app_name?: The name of the OAuth app which is used for authentication (e.g. "ip_digikey_oauth"). If this is set a connect button will be shown
* - settings_class?: The class name of the settings class which contains the settings for this provider (e.g. "App\Settings\InfoProviderSettings\DigikeySettings"). If this is set a link to the settings will be shown
*
* @phpstan-return array{ name: string, description?: string, logo?: string, url?: string, disabled_help?: string, oauth_app_name?: string, settings_class?: class-string }
*/
public function getProviderInfo(): array;
/**
* Returns a unique key for this provider, which will be saved into the database
* and used to identify the provider
* @return string A unique key for this provider (e.g. "digikey")
*/
public function getProviderKey(): string;
public function getProviderInfo(): ProviderInfoDTO;
/**
* Checks if this provider is enabled or not (meaning that it can be used for searching)
@ -76,12 +59,4 @@ interface InfoProviderInterface
* @return PartDetailDTO
*/
public function getDetails(string $id, array $options = []): PartDetailDTO;
/**
* A list of capabilities this provider supports (which kind of data it can provide).
* Not every part have to contain all of these data, but the provider should be able to provide them in general.
* Currently, this list is purely informational and not used in functional checks.
* @return ProviderCapabilities[]
*/
public function getCapabilities(): array;
}

View file

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

View file

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

View file

@ -88,6 +88,7 @@ use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Settings\InfoProviderSystem\OEMSecretsSettings;
use App\Settings\InfoProviderSystem\OEMSecretsSortMode;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@ -96,6 +97,7 @@ use Psr\Cache\CacheItemPoolInterface;
class OEMSecretsProvider implements InfoProviderInterface
{
public const PROVIDER_KEY = 'oemsecrets';
private const ENDPOINT_URL = 'https://oemsecretsapi.com/partsearch';
@ -227,37 +229,23 @@ class OEMSecretsProvider implements InfoProviderInterface
private array $distributorCountryCodes = [];
private array $countryCodeToRegionMap = [];
/**
* Get information about this provider
*
* @return array An associative array with the following keys (? means optional):
* - name: The (user friendly) name of the provider (e.g. "Digikey"), will be translated
* - description?: A short description of the provider (e.g. "Digikey is a ..."), will be translated
* - logo?: The logo of the provider (e.g. "digikey.png")
* - url?: The url of the provider (e.g. "https://www.digikey.com")
* - disabled_help?: A help text which is shown when the provider is disabled, explaining how to enable it
* - oauth_app_name?: The name of the OAuth app which is used for authentication (e.g. "ip_digikey_oauth"). If this is set a connect button will be shown
*
* @phpstan-return array{ name: string, description?: string, logo?: string, url?: string, disabled_help?: string, oauth_app_name?: string }
*/
public function getProviderInfo(): array
public function getProviderInfo(): ProviderInfoDTO
{
return [
'name' => 'OEMSecrets',
'description' => 'This provider uses the OEMSecrets API to search for parts.',
'url' => 'https://www.oemsecrets.com/',
'disabled_help' => 'Configure the API key in the provider settings to enable.',
'settings_class' => OEMSecretsSettings::class
];
}
/**
* Returns a unique key for this provider, which will be saved into the database
* and used to identify the provider
* @return string A unique key for this provider (e.g. "digikey")
*/
public function getProviderKey(): string
{
return 'oemsecrets';
return new ProviderInfoDTO(
key: self::PROVIDER_KEY,
name: 'OEMSecrets',
description: 'This provider uses the OEMSecrets API to search for parts.',
url: 'https://www.oemsecrets.com/',
disabledHelp: 'Configure the API key in the provider settings to enable.',
settingsClass: OEMSecretsSettings::class,
capabilities: [
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE,
ProviderCapabilities::DATASHEET,
ProviderCapabilities::PRICE,
ProviderCapabilities::PARAMETERS
],
);
}
/**
@ -456,17 +444,6 @@ class OEMSecretsProvider implements InfoProviderInterface
* Currently, this list is purely informational and not used in functional checks.
* @return ProviderCapabilities[]
*/
public function getCapabilities(): array
{
return [
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE,
ProviderCapabilities::DATASHEET,
ProviderCapabilities::PRICE,
];
}
/**
* Processes a single product and updates arrays for basic information, datasheets, images, parameters,
* and purchase information. Aggregates and organizes data received for a specific `part_number` and `manufacturer_id`.
@ -896,7 +873,7 @@ class OEMSecretsProvider implements InfoProviderInterface
// If there is no existing basic info array, we create a new one
if (is_null($existingBasicInfo)) {
return [
'provider_key' => $this->getProviderKey(),
'provider_key' => self::PROVIDER_KEY,
'provider_id' => $provider_id,
'name' => $product['part_number'],
'description' => $description,
@ -916,7 +893,7 @@ class OEMSecretsProvider implements InfoProviderInterface
// Update fields only if empty or undefined, with additional check for preview_image_url
return [
'provider_key' => $existingBasicInfo['provider_key'] ?? $this->getProviderKey(),
'provider_key' => $existingBasicInfo['provider_key'] ?? self::PROVIDER_KEY,
'provider_id' => $existingBasicInfo['provider_id'] ?? $provider_id,
'name' => $existingBasicInfo['name'] ?? $product['part_number'],
// Update description if it's null/empty
@ -1270,7 +1247,7 @@ class OEMSecretsProvider implements InfoProviderInterface
*/
private function generateInquiryUrl(string $partNumber, string $oemInquiry = 'compare/'): string
{
$baseUrl = rtrim($this->getProviderInfo()['url'], '/') . '/';
$baseUrl = rtrim($this->getProviderInfo()->url, '/') . '/';
$inquiryPath = trim($oemInquiry, '/') . '/';
$encodedPartNumber = urlencode(trim($partNumber));
return $baseUrl . $inquiryPath . $encodedPartNumber;

View file

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

View file

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

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.
*/
enum ProviderCapabilities
enum ProviderCapabilities: string
{
/** Basic information about a part, like the name, description, part number, manufacturer etc */
case BASIC;
case BASIC = 'BASIC';
/** Provider can provide a picture for a part */
case PICTURE;
case PICTURE = 'PICTURE';
/** Provider can provide datasheets for a part */
case DATASHEET;
case DATASHEET = 'DATASHEET';
/** Provider can provide prices for a part */
case PRICE;
case PRICE = 'PRICE';
/** Information about the footprint of a part */
case FOOTPRINT;
case FOOTPRINT = 'FOOTPRINT';
/** Provider can provide GTIN for a part */
case GTIN;
case GTIN = 'GTIN';
/** Provider can provide parameters/specifications for a part */
case PARAMETERS;
case PARAMETERS = 'PARAMETERS';
/**
* Get the order index for displaying capabilities in a stable order.

View file

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

View file

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

View file

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

View file

@ -25,7 +25,7 @@ namespace App\State\Mcp;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface;
use App\Services\InfoProviderSystem\DTOs\InfoProviderDTO;
use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
use App\Services\InfoProviderSystem\ProviderRegistry;
/**
@ -40,7 +40,7 @@ class ListInfoProvidersProcessor implements ProcessorInterface, ProviderInterfac
}
/**
* @return InfoProviderDTO[]
* @return ProviderInfoDTO[]
*/
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
{
@ -48,7 +48,7 @@ class ListInfoProvidersProcessor implements ProcessorInterface, ProviderInterfac
}
/**
* @return InfoProviderDTO[]
* @return ProviderInfoDTO[]
*/
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): array
{
@ -56,22 +56,14 @@ class ListInfoProvidersProcessor implements ProcessorInterface, ProviderInterfac
}
/**
* @return InfoProviderDTO[]
* @return ProviderInfoDTO[]
*/
private function listActiveProviders(): array
{
$result = [];
foreach ($this->providerRegistry->getActiveProviders() as $provider) {
$info = $provider->getProviderInfo();
$result[] = new InfoProviderDTO(
key: $provider->getProviderKey(),
name: $info['name'],
description: $info['description'] ?? null,
url: $info['url'] ?? null,
capabilities: array_map(static fn ($capability) => $capability->name, $provider->getCapabilities()),
);
$result[] = $provider->getProviderInfo();
}
return $result;

View file

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

View file

@ -8,35 +8,41 @@
<div class="row">
<div class="col-6">
<h5>
{% if provider.providerInfo.url is defined and provider.providerInfo.url is not empty %}
{% if provider.providerInfo.url is not empty %}
<a href="{{ provider.providerInfo.url }}" target="_blank">{{ provider.providerInfo.name }}</a>
{% else %}
{{ provider.providerInfo.name | trans }}
{{ provider.providerInfo.name }}
{% endif %}
</h5>
<div>
{% if provider.providerInfo.description is defined and provider.providerInfo.description is not null %}
{{ provider.providerInfo.description | trans }}
{% if provider.providerInfo.description is not null %}
{{ provider.providerInfo.description }}
{% endif %}
</div>
</div>
<div class="col-6">
{% if provider.providerInfo.settings_class is defined %}
<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 %}"
{% if provider.providerInfo.settingsClass is not null %}
<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 %}"
title="{% trans %}info_providers.settings.title{% endtrans %}"
><i class="fa-solid fa-cog"></i></a>
{% endif %}
{% for capability in provider.capabilities|sort((a, b) => a.orderIndex <=> b.orderIndex) %}
{% if provider.providerInfo.expensive %}
<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 #}
<span class="badge text-bg-secondary">
<i class="{{ capability.fAIconClass }} fa-fw"></i>
{{ capability.translationKey|trans }}
</span>
{% endfor %}
{% if provider.providerInfo.oauth_app_name is defined and provider.providerInfo.oauth_app_name is not empty %}
{% if provider.providerInfo.oauthAppName is not empty %}
<br>
<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>
<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>
{% endif %}
</div>
</div>
@ -44,9 +50,9 @@
<div class="row">
<div class="col text-danger">
<i class="fa-solid fa-circle-exclamation"></i> <b>{% trans %}info_providers.providers_list.disabled{% endtrans %}</b>
{% if provider.providerInfo.disabled_help is defined and provider.providerInfo.disabled_help is not empty %}
{% if provider.providerInfo.disabledHelp is not empty %}
<br>
<span class="text-muted">{{ provider.providerInfo.disabled_help|trans }}</span>
<span class="text-muted">{{ provider.providerInfo.disabledHelp }}</span>
{% endif %}
</div>
</div>

View file

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

View file

@ -39,6 +39,16 @@ class InfoProviderEndpointTest extends AuthenticatedApiTestCase
$keys = array_column($json['hydra:member'], 'key');
self::assertContains('test', $keys);
//The 'test' provider has a disabledHelp set internally, but it must not leak into the API response
$testProvider = $json['hydra:member'][array_search('test', $keys, true)];
self::assertArrayHasKey('capabilities', $testProvider);
self::assertArrayNotHasKey('disabledHelp', $testProvider);
self::assertArrayNotHasKey('oauthAppName', $testProvider);
self::assertArrayNotHasKey('settingsClass', $testProvider);
//Capabilities must serialize as plain enum-name strings, not objects
self::assertSame(['BASIC', 'FOOTPRINT'], $testProvider['capabilities']);
}
public function testListInfoProvidersRequiresAuthentication(): void

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13805,5 +13805,23 @@ Buerklin-API Authentication server:
<target>MCP endpoint</target>
</segment>
</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>
</xliff>