mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-02-28 20:39:35 +00:00
Moved barcode to info provider logic from Controller to BarcodeScanResultHandler service
This commit is contained in:
parent
caa71bbdda
commit
8dd972f1ad
4 changed files with 171 additions and 95 deletions
|
|
@ -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();
|
||||
|
|
|
|||
48
src/Exceptions/InfoProviderNotActiveException.php
Normal file
48
src/Exceptions/InfoProviderNotActiveException.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?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\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'] ?? '???');
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue