diff --git a/src/Controller/ScanController.php b/src/Controller/ScanController.php index b261f3fd..311b19f4 100644 --- a/src/Controller/ScanController.php +++ b/src/Controller/ScanController.php @@ -42,8 +42,10 @@ declare(strict_types=1); namespace App\Controller; use App\Form\LabelSystem\ScanDialogType; -use App\Services\LabelSystem\Barcodes\BarcodeNormalizer; +use App\Services\LabelSystem\Barcodes\BarcodeScanHelper; use App\Services\LabelSystem\Barcodes\BarcodeRedirector; +use App\Services\LabelSystem\Barcodes\BarcodeScanResult; +use App\Services\LabelSystem\Barcodes\BarcodeSourceType; use Doctrine\ORM\EntityNotFoundException; use InvalidArgumentException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -55,7 +57,7 @@ use Symfony\Component\Routing\Annotation\Route; #[Route(path: '/scan')] class ScanController extends AbstractController { - public function __construct(protected BarcodeRedirector $barcodeParser, protected BarcodeNormalizer $barcodeNormalizer) + public function __construct(protected BarcodeRedirector $barcodeParser, protected BarcodeScanHelper $barcodeNormalizer) { } @@ -73,10 +75,9 @@ class ScanController extends AbstractController if ($input !== null) { try { - [$type, $id] = $this->barcodeNormalizer->normalizeBarcodeContent($input); - + $scan_result = $this->barcodeNormalizer->scanBarcodeContent($input); try { - return $this->redirect($this->barcodeParser->getRedirectURL($type, $id)); + return $this->redirect($this->barcodeParser->getRedirectURL($scan_result)); } catch (EntityNotFoundException) { $this->addFlash('success', 'scan.qr_not_found'); } @@ -95,10 +96,23 @@ class ScanController extends AbstractController */ public function scanQRCode(string $type, int $id): Response { + $type = strtolower($type); + try { $this->addFlash('success', 'scan.qr_success'); - return $this->redirect($this->barcodeParser->getRedirectURL($type, $id)); + if (!isset(BarcodeScanHelper::QR_TYPE_MAP[$type])) { + throw new InvalidArgumentException('Unknown type: '.$type); + } + //Construct the scan result manually, as we don't have a barcode here + $scan_result = new BarcodeScanResult( + target_type: BarcodeScanHelper::QR_TYPE_MAP[$type], + target_id: $id, + //The routes are only used on the internal generated QR codes + source_type: BarcodeSourceType::INTERNAL + ); + + return $this->redirect($this->barcodeParser->getRedirectURL($scan_result)); } catch (EntityNotFoundException) { $this->addFlash('success', 'scan.qr_not_found'); diff --git a/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php b/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php index 0eba0ed4..bc21b787 100644 --- a/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php +++ b/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php @@ -41,6 +41,7 @@ declare(strict_types=1); namespace App\Services\LabelSystem\Barcodes; +use App\Entity\LabelSystem\LabelSupportedElement; use App\Entity\Parts\PartLot; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityNotFoundException; @@ -59,32 +60,30 @@ final class BarcodeRedirector /** * Determines the URL to which the user should be redirected, when scanning a QR code. * - * @param string $type The type of the element that was scanned (e.g. 'part', 'lot', etc.) - * @param int $id The ID of the element that was scanned - * + * @param BarcodeScanResult $barcodeScan The result of the barcode scan * @return string the URL to which should be redirected * * @throws EntityNotFoundException */ - public function getRedirectURL(string $type, int $id): string + public function getRedirectURL(BarcodeScanResult $barcodeScan): string { - switch ($type) { - case 'part': - return $this->urlGenerator->generate('app_part_show', ['id' => $id]); - case 'lot': + switch ($barcodeScan->target_type) { + case LabelSupportedElement::PART: + return $this->urlGenerator->generate('app_part_show', ['id' => $barcodeScan->target_id]); + case LabelSupportedElement::PART_LOT: //Try to determine the part to the given lot - $lot = $this->em->find(PartLot::class, $id); + $lot = $this->em->find(PartLot::class, $barcodeScan->target_id); if (!$lot instanceof PartLot) { throw new EntityNotFoundException(); } return $this->urlGenerator->generate('app_part_show', ['id' => $lot->getPart()->getID()]); - case 'location': - return $this->urlGenerator->generate('part_list_store_location', ['id' => $id]); + case LabelSupportedElement::STORELOCATION: + return $this->urlGenerator->generate('part_list_store_location', ['id' => $barcodeScan->target_id]); default: - throw new InvalidArgumentException('Unknown $type: '.$type); + throw new InvalidArgumentException('Unknown $type: '.$barcodeScan->target_type->name); } } } diff --git a/src/Services/LabelSystem/Barcodes/BarcodeNormalizer.php b/src/Services/LabelSystem/Barcodes/BarcodeScanHelper.php similarity index 53% rename from src/Services/LabelSystem/Barcodes/BarcodeNormalizer.php rename to src/Services/LabelSystem/Barcodes/BarcodeScanHelper.php index a5b6cb5e..b2dcdac7 100644 --- a/src/Services/LabelSystem/Barcodes/BarcodeNormalizer.php +++ b/src/Services/LabelSystem/Barcodes/BarcodeScanHelper.php @@ -41,24 +41,59 @@ declare(strict_types=1); namespace App\Services\LabelSystem\Barcodes; +use App\Entity\LabelSystem\LabelSupportedElement; use InvalidArgumentException; /** * @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeNormalizerTest */ -final class BarcodeNormalizer +final class BarcodeScanHelper { private const PREFIX_TYPE_MAP = [ - 'L' => 'lot', - 'P' => 'part', - 'S' => 'location', + 'L' => LabelSupportedElement::PART_LOT, + 'P' => LabelSupportedElement::PART, + 'S' => LabelSupportedElement::STORELOCATION, + ]; + + public const QR_TYPE_MAP = [ + 'lot' => LabelSupportedElement::PART_LOT, + 'part' => LabelSupportedElement::PART, + 'location' => LabelSupportedElement::STORELOCATION, ]; /** - * Parses barcode content and normalizes it. - * Returns an array in the format ['part', 1]: First entry contains element type, second the ID of the element. + * Parse the given barcode content and return the target type and ID. + * If the barcode could not be parsed, an exception is thrown. + * Using the $type parameter, you can specify how the barcode should be parsed. If set to null, the function + * will try to guess the type. + * @param string $input + * @param BarcodeSourceType|null $type + * @return BarcodeScanResult */ - public function normalizeBarcodeContent(string $input): array + public function scanBarcodeContent(string $input, ?BarcodeSourceType $type = null): BarcodeScanResult + { + //Do specific parsing + if ($type === BarcodeSourceType::INTERNAL) { + return $this->parseInternalBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); + } + + //Null means auto and we try the different formats + $result = $this->parseInternalBarcode($input); + + if ($result !== null) { + return $result; + } + throw new InvalidArgumentException('Unknown barcode format'); + } + + /** + * This function tries to interpret the given barcode content as an internal barcode. + * If the barcode could not be parsed at all, null is returned. If the barcode is a valid format, but could + * not be found in the database, an exception is thrown. + * @param string $input + * @return BarcodeScanResult|null + */ + private function parseInternalBarcode(string $input): ?BarcodeScanResult { $input = trim($input); $matches = []; @@ -68,7 +103,11 @@ final class BarcodeNormalizer //Extract parts from QR code's URL if (preg_match('#^https?://.*/scan/(\w+)/(\d+)/?$#', $input, $matches)) { - return [$matches[1], (int) $matches[2]]; + return new BarcodeScanResult( + target_type: self::QR_TYPE_MAP[strtolower($matches[1])], + target_id: (int) $matches[2], + source_type: BarcodeSourceType::INTERNAL + ); } //New Code39 barcode use L0001 format @@ -80,7 +119,11 @@ final class BarcodeNormalizer throw new InvalidArgumentException('Unknown prefix '.$prefix); } - return [self::PREFIX_TYPE_MAP[$prefix], $id]; + return new BarcodeScanResult( + target_type: self::PREFIX_TYPE_MAP[$prefix], + target_id: $id, + source_type: BarcodeSourceType::INTERNAL + ); } //During development the L-000001 format was used @@ -92,19 +135,32 @@ final class BarcodeNormalizer throw new InvalidArgumentException('Unknown prefix '.$prefix); } - return [self::PREFIX_TYPE_MAP[$prefix], $id]; + return new BarcodeScanResult( + target_type: self::PREFIX_TYPE_MAP[$prefix], + target_id: $id, + source_type: BarcodeSourceType::INTERNAL + ); } //Legacy Part-DB location labels used $L00336 format if (preg_match('#^\$L(\d{5,})$#', $input, $matches)) { - return ['location', (int) $matches[1]]; + return new BarcodeScanResult( + target_type: LabelSupportedElement::STORELOCATION, + target_id: (int) $matches[1], + source_type: BarcodeSourceType::INTERNAL + ); } //Legacy Part-DB used EAN8 barcodes for part labels. Format 0000001(2) (note the optional 8th digit => checksum) if (preg_match('#^(\d{7})\d?$#', $input, $matches)) { - return ['part', (int) $matches[1]]; + return new BarcodeScanResult( + target_type: LabelSupportedElement::PART, + target_id: (int) $matches[1], + source_type: BarcodeSourceType::INTERNAL + ); } - throw new InvalidArgumentException('Unknown barcode format!'); + //This function abstain from further parsing + return null; } } diff --git a/src/Services/LabelSystem/Barcodes/BarcodeScanResult.php b/src/Services/LabelSystem/Barcodes/BarcodeScanResult.php new file mode 100644 index 00000000..7f1315b3 --- /dev/null +++ b/src/Services/LabelSystem/Barcodes/BarcodeScanResult.php @@ -0,0 +1,39 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\LabelSystem\Barcodes; + +use App\Entity\LabelSystem\LabelSupportedElement; + +/** + * This class represents the result of a barcode scan, with the target type and the ID of the element + */ +class BarcodeScanResult +{ + public function __construct( + public readonly LabelSupportedElement $target_type, + public readonly int $target_id, + public readonly BarcodeSourceType $source_type, + ) { + } +} \ No newline at end of file diff --git a/src/Services/LabelSystem/Barcodes/BarcodeSourceType.php b/src/Services/LabelSystem/Barcodes/BarcodeSourceType.php new file mode 100644 index 00000000..c2573152 --- /dev/null +++ b/src/Services/LabelSystem/Barcodes/BarcodeSourceType.php @@ -0,0 +1,33 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\LabelSystem\Barcodes; + +/** + * This enum represents the different types, where a barcode/QR-code can be generated from + */ +enum BarcodeSourceType +{ + /** This Barcode was generated using Part-DB internal recommended barcode generator */ + case INTERNAL; +} \ No newline at end of file diff --git a/tests/Services/LabelSystem/Barcodes/BarcodeNormalizerTest.php b/tests/Services/LabelSystem/Barcodes/BarcodeNormalizerTest.php deleted file mode 100644 index 45b50389..00000000 --- a/tests/Services/LabelSystem/Barcodes/BarcodeNormalizerTest.php +++ /dev/null @@ -1,115 +0,0 @@ -. - */ - -declare(strict_types=1); - -/** - * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). - * - * Copyright (C) 2019 - 2020 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 . - */ - -namespace App\Tests\Services\LabelSystem\Barcodes; - -use App\Services\LabelSystem\Barcodes\BarcodeNormalizer; -use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - -class BarcodeNormalizerTest extends WebTestCase -{ - /** - * @var BarcodeNormalizer - */ - protected $service; - - protected function setUp(): void - { - self::bootKernel(); - $this->service = self::getContainer()->get(BarcodeNormalizer::class); - } - - public function dataProvider(): \Iterator - { - //QR URL content: - yield [['lot', 1], 'https://localhost:8000/scan/lot/1']; - yield [['part', 123], 'https://localhost:8000/scan/part/123']; - yield [['location', 4], 'http://foo.bar/part-db/scan/location/4']; - yield [['under_score', 10], 'http://test/part-db/sub/scan/under_score/10/']; - //Current Code39 format: - yield [['lot', 10], 'L0010']; - yield [['lot', 123], 'L0123']; - yield [['lot', 123456], 'L123456']; - yield [['part', 2], 'P0002']; - //Development phase Code39 barcodes: - yield [['lot', 10], 'L-000010']; - yield [['lot', 10], 'Lß000010']; - yield [['part', 123], 'P-000123']; - yield [['location', 123], 'S-000123']; - yield [['lot', 12_345_678], 'L-12345678']; - //Legacy storelocation format - yield [['location', 336], '$L00336']; - yield [['location', 12_345_678], '$L12345678']; - //Legacy Part format - yield [['part', 123], '0000123']; - yield [['part', 123], '00001236']; - yield [['part', 1_234_567], '12345678']; - } - - public function invalidDataProvider(): array - { - return [ - ['https://localhost/part/1'], //Without scan - ['L-'], //Without number - ['L-123'], //Too short - ['X-123456'], //Unknown prefix - ['XXXWADSDF sdf'], //Garbage - [''], //Empty - ]; - } - - /** - * @dataProvider dataProvider - */ - public function testNormalizeBarcodeContent(array $expected, string $input): void - { - $this->assertSame($expected, $this->service->normalizeBarcodeContent($input)); - } - - /** - * @dataProvider invalidDataProvider - */ - public function testInvalidFormats(string $input): void - { - $this->expectException(\InvalidArgumentException::class); - $this->service->normalizeBarcodeContent($input); - } -} diff --git a/tests/Services/LabelSystem/Barcodes/BarcodeRedirectorTest.php b/tests/Services/LabelSystem/Barcodes/BarcodeRedirectorTest.php index dbbd958c..08390896 100644 --- a/tests/Services/LabelSystem/Barcodes/BarcodeRedirectorTest.php +++ b/tests/Services/LabelSystem/Barcodes/BarcodeRedirectorTest.php @@ -41,13 +41,16 @@ declare(strict_types=1); namespace App\Tests\Services\LabelSystem\Barcodes; +use App\Entity\LabelSystem\LabelSupportedElement; use App\Services\LabelSystem\Barcodes\BarcodeRedirector; +use App\Services\LabelSystem\Barcodes\BarcodeScanResult; +use App\Services\LabelSystem\Barcodes\BarcodeSourceType; use Doctrine\ORM\EntityNotFoundException; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -class BarcodeRedirectorTest extends KernelTestCase +final class BarcodeRedirectorTest extends KernelTestCase { - private ?object $service = null; + private ?BarcodeRedirector $service = null; protected function setUp(): void { @@ -55,13 +58,13 @@ class BarcodeRedirectorTest extends KernelTestCase $this->service = self::getContainer()->get(BarcodeRedirector::class); } - public function urlDataProvider(): array + public static function urlDataProvider(): array { return [ - ['part', '/en/part/1'], - //Part lot redirects to Part info page (Part lot 1 is associated with part 3 - ['lot', '/en/part/3'], - ['location', '/en/store_location/1/parts'], + [new BarcodeScanResult(LabelSupportedElement::PART, 1, BarcodeSourceType::INTERNAL), '/en/part/1'], + //Part lot redirects to Part info page (Part lot 1 is associated with part 3) + [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 1, BarcodeSourceType::INTERNAL), '/en/part/3'], + [new BarcodeScanResult(LabelSupportedElement::STORELOCATION, 1, BarcodeSourceType::INTERNAL), '/en/store_location/1/parts'], ]; } @@ -69,21 +72,16 @@ class BarcodeRedirectorTest extends KernelTestCase * @dataProvider urlDataProvider * @group DB */ - public function testGetRedirectURL(string $type, string $url): void + public function testGetRedirectURL(BarcodeScanResult $scanResult, string $url): void { - $this->assertSame($url, $this->service->getRedirectURL($type, 1)); + $this->assertSame($url, $this->service->getRedirectURL($scanResult)); } public function testGetRedirectEntityNotFount(): void { $this->expectException(EntityNotFoundException::class); //If we encounter an invalid lot, we must throw an exception - $this->service->getRedirectURL('lot', 12_345_678); - } - - public function testInvalidType(): void - { - $this->expectException(\InvalidArgumentException::class); - $this->service->getRedirectURL('invalid', 1); + $this->service->getRedirectURL(new BarcodeScanResult(LabelSupportedElement::PART_LOT, + 12_345_678, BarcodeSourceType::INTERNAL)); } } diff --git a/tests/Services/LabelSystem/Barcodes/BarcodeScanHelperTest.php b/tests/Services/LabelSystem/Barcodes/BarcodeScanHelperTest.php new file mode 100644 index 00000000..9d803202 --- /dev/null +++ b/tests/Services/LabelSystem/Barcodes/BarcodeScanHelperTest.php @@ -0,0 +1,136 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2020 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 . + */ + +namespace App\Tests\Services\LabelSystem\Barcodes; + +use App\Entity\LabelSystem\LabelSupportedElement; +use App\Services\LabelSystem\Barcodes\BarcodeScanHelper; +use App\Services\LabelSystem\Barcodes\BarcodeScanResult; +use App\Services\LabelSystem\Barcodes\BarcodeSourceType; +use Com\Tecnick\Barcode\Barcode; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class BarcodeScanHelperTest extends WebTestCase +{ + private ?BarcodeScanHelper $service = null; + + protected function setUp(): void + { + self::bootKernel(); + $this->service = self::getContainer()->get(BarcodeScanHelper::class); + } + + public static function dataProvider(): \Iterator + { + //QR URL content: + yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 1, BarcodeSourceType::INTERNAL), + 'https://localhost:8000/scan/lot/1']; + yield [new BarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), + 'https://localhost:8000/scan/part/123']; + yield [new BarcodeScanResult(LabelSupportedElement::STORELOCATION, 4, BarcodeSourceType::INTERNAL), + 'http://foo.bar/part-db/scan/location/4']; + + //Current Code39 format: + yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL), + 'L0010']; + yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 123, BarcodeSourceType::INTERNAL), + 'L0123']; + yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 123456, BarcodeSourceType::INTERNAL), + 'L123456']; + yield [new BarcodeScanResult(LabelSupportedElement::PART, 2, BarcodeSourceType::INTERNAL), + 'P0002']; + + //Development phase Code39 barcodes: + yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL), + 'L-000010']; + yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL), + 'Lß000010']; + yield [new BarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), + 'P-000123']; + yield [new BarcodeScanResult(LabelSupportedElement::STORELOCATION, 123, BarcodeSourceType::INTERNAL), + 'S-000123']; + yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 12_345_678, BarcodeSourceType::INTERNAL), + 'L-12345678']; + + //Legacy storelocation format + yield [new BarcodeScanResult(LabelSupportedElement::STORELOCATION, 336, BarcodeSourceType::INTERNAL), + '$L00336']; + yield [new BarcodeScanResult(LabelSupportedElement::STORELOCATION, 12_345_678, BarcodeSourceType::INTERNAL), + '$L12345678']; + + //Legacy Part format + yield [new BarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), + '0000123']; + yield [new BarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), + '00001236']; + yield [new BarcodeScanResult(LabelSupportedElement::PART, 1_234_567, BarcodeSourceType::INTERNAL), + '12345678']; + } + + public static function invalidDataProvider(): array + { + return [ + ['https://localhost/part/1'], //Without scan + ['L-'], //Without number + ['L-123'], //Too short + ['X-123456'], //Unknown prefix + ['XXXWADSDF sdf'], //Garbage + [''], //Empty + ]; + } + + /** + * @dataProvider dataProvider + */ + public function testNormalizeBarcodeContent(BarcodeScanResult $expected, string $input): void + { + $this->assertEquals($expected, $this->service->scanBarcodeContent($input)); + } + + /** + * @dataProvider invalidDataProvider + */ + public function testInvalidFormats(string $input): void + { + $this->expectException(\InvalidArgumentException::class); + $this->service->scanBarcodeContent($input); + } +}