diff --git a/src/Controller/ScanController.php b/src/Controller/ScanController.php index 55bf2e22..55f6429b 100644 --- a/src/Controller/ScanController.php +++ b/src/Controller/ScanController.php @@ -41,6 +41,7 @@ declare(strict_types=1); namespace App\Controller; +use App\Exceptions\InfoProviderNotActiveException; use App\Form\LabelSystem\ScanDialogType; use App\Services\InfoProviderSystem\Providers\LCSCProvider; use App\Services\LabelSystem\BarcodeScanner\BarcodeScanResultHandler; @@ -71,11 +72,11 @@ use \App\Entity\Parts\StorageLocation; class ScanController extends AbstractController { public function __construct( - protected BarcodeScanResultHandler $barcodeParser, - protected BarcodeScanHelper $barcodeNormalizer, + protected BarcodeScanResultHandler $resultHandler, + protected BarcodeScanHelper $barcodeNormalizer, private readonly ProviderRegistry $providerRegistry, private readonly PartInfoRetriever $infoRetriever, - ) {} + ) {} #[Route(path: '', name: 'scan_dialog')] public function dialog(Request $request, #[MapQueryParameter] ?string $input = null): Response @@ -103,7 +104,7 @@ class ScanController extends AbstractController // If not in info mode, mimic “normal scan” behavior: redirect if possible. if (!$infoMode) { try { - $url = $this->barcodeParser->getInfoURL($scan); + $url = $this->resultHandler->getInfoURL($scan); return $this->redirect($url); } catch (EntityNotFoundException) { // Decoded OK, but no part is found. If it’s a vendor code, redirect to create. @@ -153,7 +154,7 @@ class ScanController extends AbstractController source_type: BarcodeSourceType::INTERNAL ); - return $this->redirect($this->barcodeParser->getInfoURL($scan_result)); + return $this->redirect($this->resultHandler->getInfoURL($scan_result)); } catch (EntityNotFoundException) { $this->addFlash('success', 'scan.qr_not_found'); @@ -168,86 +169,13 @@ class ScanController extends AbstractController */ private function buildCreateUrlForScanResult(BarcodeScanResultInterface $scanResult): ?string { - // LCSC - if ($scanResult instanceof LCSCBarcodeScanResult) { - $lcscCode = $scanResult->lcscCode; - if ($lcscCode !== null && $lcscCode !== '') { - return $this->generateUrl('info_providers_create_part', [ - 'providerKey' => 'lcsc', - 'providerId' => $lcscCode, - ]); - } - } - - // Mouser / Digi-Key (EIGP114) - if ($scanResult instanceof EIGP114BarcodeScanResult) { - $vendor = $scanResult->guessBarcodeVendor(); - - // Mouser: use supplierPartNumber -> search provider -> provider_id - if ($vendor === 'mouser' - && $scanResult->supplierPartNumber !== null - && $scanResult->supplierPartNumber !== '' - ) { - try { - $mouserProvider = $this->providerRegistry->getProviderByKey('mouser'); - - if (!$mouserProvider->isActive()) { - $this->addFlash('warning', 'Mouser provider is disabled / not configured.'); - return null; - } - // Search Mouser using the MPN - $dtos = $this->infoRetriever->searchByKeyword( - keyword: $scanResult->supplierPartNumber, - providers: [$mouserProvider] - ); - - // If there are results, provider_id is MouserPartNumber (per MouserProvider.php) - $best = $dtos[0] ?? null; - - if ($best !== null && $best->provider_id !== '') { - - return $this->generateUrl('info_providers_create_part', [ - 'providerKey' => 'mouser', - 'providerId' => $best->provider_id, - ]); - } - - $this->addFlash('warning', 'No Mouser match found for this MPN.'); - return null; - } catch (\InvalidArgumentException) { - // provider key not found in registry - $this->addFlash('warning', 'Mouser provider is not installed/enabled.'); - return null; - } catch (\Throwable $e) { - // Don’t break scanning UX if provider lookup fails - $this->addFlash('warning', 'Mouser lookup failed: ' . $e->getMessage()); - return null; - } - } - - // Digi-Key: can use customerPartNumber or supplierPartNumber directly - if ($vendor === 'digikey') { - try { - $provider = $this->providerRegistry->getProviderByKey('digikey'); - - if (!$provider->isActive()) { - $this->addFlash('warning', 'Digi-Key provider is disabled / not configured (API key missing).'); - return null; - } - - $id = $scanResult->customerPartNumber ?: $scanResult->supplierPartNumber; - - if (is_string($id) && $id !== '') { - return $this->generateUrl('info_providers_create_part', [ - 'providerKey' => 'digikey', - 'providerId' => $id, - ]); - } - } catch (\InvalidArgumentException) { - $this->addFlash('warning', 'Digi-Key provider is not installed/enabled'); - return null; - } - } + try { + return $this->resultHandler->getCreationURL($scanResult); + } catch (InfoProviderNotActiveException $e) { + $this->addFlash('error', $e->getMessage()); + } catch (\Throwable) { + $this->addFlash('error', 'An error occurred while looking up the provider for this barcode. Please try again later.'); + // Don’t break scanning UX if provider lookup fails } return null; @@ -338,7 +266,7 @@ class ScanController extends AbstractController $targetFound = false; try { - $redirectUrl = $this->barcodeParser->getInfoURL($scan); + $redirectUrl = $this->resultHandler->getInfoURL($scan); $targetFound = true; } catch (EntityNotFoundException) { } @@ -350,7 +278,7 @@ class ScanController extends AbstractController $locations = []; if ($targetFound) { - $part = $this->barcodeParser->resolvePart($scan); + $part = $this->resultHandler->resolvePart($scan); if ($part instanceof Part) { $partName = $part->getName(); diff --git a/src/Exceptions/InfoProviderNotActiveException.php b/src/Exceptions/InfoProviderNotActiveException.php new file mode 100644 index 00000000..02f7cfb7 --- /dev/null +++ b/src/Exceptions/InfoProviderNotActiveException.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Exceptions; + +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; + +/** + * An exception denoting that a required info provider is not active. This can be used to display a user-friendly error message, + * when a user tries to use an info provider that is not active. + */ +class InfoProviderNotActiveException extends \RuntimeException +{ + public function __construct(public readonly string $providerKey, public readonly string $friendlyName) + { + parent::__construct(sprintf('The info provider "%s" (%s) is not active.', $this->friendlyName, $this->providerKey)); + } + + /** + * Creates an instance of this exception from an info provider instance + * @param InfoProviderInterface $provider + * @return self + */ + public static function fromProvider(InfoProviderInterface $provider): self + { + return new self($provider->getProviderKey(), $provider->getProviderInfo()['name'] ?? '???'); + } +} diff --git a/src/Services/InfoProviderSystem/PartInfoRetriever.php b/src/Services/InfoProviderSystem/PartInfoRetriever.php index 9a24f3ae..27474b92 100644 --- a/src/Services/InfoProviderSystem/PartInfoRetriever.php +++ b/src/Services/InfoProviderSystem/PartInfoRetriever.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem; use App\Entity\Parts\Part; +use App\Exceptions\InfoProviderNotActiveException; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; @@ -49,6 +50,7 @@ final class PartInfoRetriever * @param string[]|InfoProviderInterface[] $providers A list of providers to search in, either as provider keys or as provider instances * @param string $keyword The keyword to search for * @return SearchResultDTO[] The search results + * @throws InfoProviderNotActiveException if any of the given providers is not active */ public function searchByKeyword(string $keyword, array $providers): array { @@ -61,7 +63,7 @@ final class PartInfoRetriever //Ensure that the provider is active if (!$provider->isActive()) { - throw new \RuntimeException("The provider with key {$provider->getProviderKey()} is not active!"); + throw InfoProviderNotActiveException::fromProvider($provider); } if (!$provider instanceof InfoProviderInterface) { @@ -97,6 +99,7 @@ final class PartInfoRetriever * @param string $provider_key * @param string $part_id * @return PartDetailDTO + * @throws InfoProviderNotActiveException if the the given providers is not active */ public function getDetails(string $provider_key, string $part_id): PartDetailDTO { @@ -104,7 +107,7 @@ final class PartInfoRetriever //Ensure that the provider is active if (!$provider->isActive()) { - throw new \RuntimeException("The provider with key $provider_key is not active!"); + throw InfoProviderNotActiveException::fromProvider($provider); } //Generate key and escape reserved characters from the provider id diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultHandler.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultHandler.php index b7293ca7..3f868cf7 100644 --- a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultHandler.php +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultHandler.php @@ -46,7 +46,10 @@ use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use App\Entity\Parts\StorageLocation; +use App\Exceptions\InfoProviderNotActiveException; use App\Repository\Parts\PartRepository; +use App\Services\InfoProviderSystem\PartInfoRetriever; +use App\Services\InfoProviderSystem\ProviderRegistry; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityNotFoundException; use InvalidArgumentException; @@ -59,7 +62,8 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; */ final readonly class BarcodeScanResultHandler { - public function __construct(private UrlGeneratorInterface $urlGenerator, private EntityManagerInterface $em) + public function __construct(private UrlGeneratorInterface $urlGenerator, private EntityManagerInterface $em, private PartInfoRetriever $infoRetriever, + private ProviderRegistry $providerRegistry) { } @@ -96,11 +100,33 @@ final readonly class BarcodeScanResultHandler throw new \LogicException("Resolved entity is of unknown type: ".get_class($entity)); } + /** + * Returns a URL to create a new part based on this barcode scan result, if possible. + * @param BarcodeScanResultInterface $scanResult + * @return string|null + * @throws InfoProviderNotActiveException If the scan result contains information for a provider which is currently not active in the system + */ + public function getCreationURL(BarcodeScanResultInterface $scanResult): ?string + { + $infos = $this->getCreateInfos($scanResult); + if ($infos === null) { + return null; + } + + //Ensure that the provider is active, otherwise we should not generate a creation URL for it + $provider = $this->providerRegistry->getProviderByKey($infos['providerKey']); + if (!$provider->isActive()) { + throw InfoProviderNotActiveException::fromProvider($provider); + } + + return $this->urlGenerator->generate('info_providers_create_part', ['providerKey' => $infos['providerKey'], 'providerId' => $infos['providerId']]); + } + /** * Tries to resolve the given barcode scan result to a local entity. This can be a Part, a PartLot or a StorageLocation, depending on the type of the barcode and the information contained in it. * Returns null if no matching entity could be found. * @param BarcodeScanResultInterface $barcodeScan - * @return Part|PartLot|StorageLocation + * @return Part|PartLot|StorageLocation|null */ public function resolveEntity(BarcodeScanResultInterface $barcodeScan): Part|PartLot|StorageLocation|null { @@ -113,7 +139,7 @@ final readonly class BarcodeScanResultHandler } if ($barcodeScan instanceof GTINBarcodeScanResult) { - return $this->resolvePartFromGTIN($barcodeScan); + return $this->em->getRepository(Part::class)->findOneBy(['gtin' => $barcodeScan->gtin]); } if ($barcodeScan instanceof LCSCBarcodeScanResult) { @@ -210,11 +236,82 @@ final readonly class BarcodeScanResultHandler return $this->em->getRepository(Part::class)->getPartByMPN($pm); } - private function resolvePartFromGTIN(GTINBarcodeScanResult $barcodeScan): ?Part + + /** + * Tries to extract creation information for a part from the given barcode scan result. This can be used to + * automatically fill in the info provider reference of a part, when creating a new part based on the scan result. + * Returns null if no provider information could be extracted from the scan result, or if the scan result type is unknown and cannot be handled by this function. + * It is not necessarily checked that the provider is active, or that the result actually exists on the provider side. + * @param BarcodeScanResultInterface $scanResult + * @return array{providerKey: string, providerId: string}|null + * @throws InfoProviderNotActiveException If the scan result contains information for a provider which is currently not active in the system + */ + public function getCreateInfos(BarcodeScanResultInterface $scanResult): ?array { - return $this->em->getRepository(Part::class)->findOneBy(['gtin' => $barcodeScan->gtin]); + // LCSC + if ($scanResult instanceof LCSCBarcodeScanResult) { + return [ + 'providerKey' => 'lcsc', + 'providerId' => $scanResult->lcscCode, + ]; + } + + if ($scanResult instanceof EIGP114BarcodeScanResult) { + return $this->getCreationInfoForEIGP114($scanResult); + } + + return null; + + } + + /** + * @param EIGP114BarcodeScanResult $scanResult + * @return array{providerKey: string, providerId: string}|null + */ + private function getCreationInfoForEIGP114(EIGP114BarcodeScanResult $scanResult): ?array + { + $vendor = $scanResult->guessBarcodeVendor(); + + // Mouser: use supplierPartNumber -> search provider -> provider_id + if ($vendor === 'mouser' && $scanResult->supplierPartNumber !== null + ) { + // Search Mouser using the MPN + $dtos = $this->infoRetriever->searchByKeyword( + keyword: $scanResult->supplierPartNumber, + providers: ["mouser"] + ); + + // If there are results, provider_id is MouserPartNumber (per MouserProvider.php) + $best = $dtos[0] ?? null; + + if ($best !== null) { + return [ + 'providerKey' => 'mouser', + 'providerId' => $best->provider_id, + ]; + } + + return null; + } + + // Digi-Key: can use customerPartNumber or supplierPartNumber directly + if ($vendor === 'digikey') { + return [ + 'providerKey' => 'digikey', + 'providerId' => $scanResult->customerPartNumber ?? $scanResult->supplierPartNumber, + ]; + } + + // Element14: can use supplierPartNumber directly + if ($vendor === 'element14') { + return [ + 'providerKey' => 'element14', + 'providerId' => $scanResult->supplierPartNumber, + ]; + } + + return null; } - }