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);
+ }
+}