Merge branch 'master' into settings-bundle

This commit is contained in:
Jan Böhmer 2025-01-17 22:06:18 +01:00
commit 8750573724
191 changed files with 27745 additions and 12133 deletions

View file

@ -47,7 +47,7 @@ class MoneyFormatter
public function format(string|float $value, ?Currency $currency = null, int $decimals = 5, bool $show_all_digits = false): string
{
$iso_code = $this->localizationSettings->baseCurrency;
if ($currency instanceof Currency && ($currency->getIsoCode() !== null && $currency->getIsoCode() !== '')) {
if ($currency instanceof Currency && ($currency->getIsoCode() !== '')) {
$iso_code = $currency->getIsoCode();
}

View file

@ -153,6 +153,7 @@ class BOMImporter
break;
}
//@phpstan-ignore-next-line We want to keep this check just to be safe when something changes
$new_index = self::MAP_KICAD_PCB_FIELDS[$index] ?? throw new \UnexpectedValueException('Invalid field index!');
$out[$new_index] = $field;
}

View file

@ -44,6 +44,12 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
*/
class EntityImporter
{
/**
* The encodings that are supported by the importer, and that should be autodeceted.
*/
private const ENCODINGS = ["ASCII", "UTF-8", "ISO-8859-1", "ISO-8859-15", "Windows-1252", "UTF-16", "UTF-32"];
public function __construct(protected SerializerInterface $serializer, protected EntityManagerInterface $em, protected ValidatorInterface $validator)
{
}
@ -58,13 +64,16 @@ class EntityImporter
* @phpstan-param class-string<T> $class_name
* @param AbstractStructuralDBElement|null $parent the element which will be used as parent element for new elements
* @param array $errors an associative array containing all validation errors
* @param-out array<string, array{'entity': object, 'violations': ConstraintViolationListInterface}> $errors
* @param-out list<array{'entity': object, 'violations': ConstraintViolationListInterface}> $errors
*
* @return AbstractNamedDBElement[] An array containing all valid imported entities (with the type $class_name)
* @return T[]
*/
public function massCreation(string $lines, string $class_name, ?AbstractStructuralDBElement $parent = null, array &$errors = []): array
{
//Try to detect the text encoding of the data and convert it to UTF-8
$lines = mb_convert_encoding($lines, 'UTF-8', mb_detect_encoding($lines, self::ENCODINGS));
//Expand every line to a single entry:
$names = explode("\n", $lines);
@ -124,13 +133,15 @@ class EntityImporter
if ($repo instanceof StructuralDBElementRepository) {
$entities = $repo->getNewEntityFromPath($new_path);
$entity = end($entities);
if ($entity === false) {
throw new InvalidArgumentException('getNewEntityFromPath returned an empty array!');
}
} else { //Otherwise just create a new entity
$entity = new $class_name;
$entity->setName($name);
}
//Validate entity
$tmp = $this->validator->validate($entity);
//If no error occured, write entry to DB:
@ -159,6 +170,9 @@ class EntityImporter
*/
public function importString(string $data, array $options = [], array &$errors = []): array
{
//Try to detect the text encoding of the data and convert it to UTF-8
$data = mb_convert_encoding($data, 'UTF-8', mb_detect_encoding($data, self::ENCODINGS));
$resolver = new OptionsResolver();
$this->configureOptions($resolver);
$options = $resolver->resolve($options);
@ -215,6 +229,11 @@ class EntityImporter
//Iterate over each $entity write it to DB.
foreach ($entities as $key => $entity) {
//Ensure that entity is a NamedDBElement
if (!$entity instanceof AbstractNamedDBElement) {
throw new \RuntimeException("Encountered an entity that is not a NamedDBElement!");
}
//Validate entity
$tmp = $this->validator->validate($entity);
@ -269,7 +288,7 @@ class EntityImporter
*
* @param File $file the file that should be used for importing
* @param array $options options for the import process
* @param AbstractNamedDBElement[] $entities The imported entities are returned in this array
* @param-out AbstractNamedDBElement[] $entities The imported entities are returned in this array
*
* @return array<string, array{'entity': object, 'violations': ConstraintViolationListInterface}> An associative array containing an ConstraintViolationList and the entity name as key are returned,
* if an error happened during validation. When everything was successfully, the array should be empty.
@ -305,7 +324,7 @@ class EntityImporter
* @param array $options options for the import process
* @param-out array<string, array{'entity': object, 'violations': ConstraintViolationListInterface}> $errors
*
* @return array an array containing the deserialized elements
* @return AbstractNamedDBElement[] an array containing the deserialized elements
*/
public function importFile(File $file, array $options = [], array &$errors = []): array
{

View file

@ -205,10 +205,6 @@ trait PKImportHelperTrait
*/
protected function setIDOfEntity(AbstractDBElement $element, int|string $id): void
{
if (!is_int($id) && !is_string($id)) {
throw new \InvalidArgumentException('ID must be an integer or string');
}
$id = (int) $id;
$metadata = $this->em->getClassMetadata($element::class);

View file

@ -27,6 +27,7 @@ use App\Entity\Attachments\AttachmentType;
use App\Entity\Attachments\PartAttachment;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parameters\PartParameter;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\InfoProviderReference;
use App\Entity\Parts\Manufacturer;
@ -36,6 +37,7 @@ use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Currency;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
use App\Repository\Parts\CategoryRepository;
use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
@ -160,6 +162,12 @@ final class DTOtoEntityConverter
$entity->setMass($dto->mass);
//Try to map the category to an existing entity (but never create a new one)
if ($dto->category) {
//@phpstan-ignore-next-line For some reason php does not recognize the repo returns a category
$entity->setCategory($this->em->getRepository(Category::class)->findForInfoProvider($dto->category));
}
$entity->setManufacturer($this->getOrCreateEntity(Manufacturer::class, $dto->manufacturer));
$entity->setFootprint($this->getOrCreateEntity(Footprint::class, $dto->footprint));

View file

@ -0,0 +1,77 @@
<?php
namespace App\Services\InfoProviderSystem;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Part;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use Doctrine\ORM\EntityManagerInterface;
/**
* This service assists in finding existing local parts for a SearchResultDTO, so that the user
* does not accidentally add a duplicate.
*
* A part is considered to be a duplicate, if the provider reference matches, or if the manufacturer and the MPN of the
* DTO and the local part match. This checks also for alternative names of the manufacturer and the part name (as alternative
* for the MPN).
*/
final class ExistingPartFinder
{
public function __construct(private readonly EntityManagerInterface $em)
{
}
/**
* Return the first existing local part, that matches the search result.
* If no part is found, return null.
* @param SearchResultDTO $dto
* @return Part|null
*/
public function findFirstExisting(SearchResultDTO $dto): ?Part
{
$results = $this->findAllExisting($dto);
return count($results) > 0 ? $results[0] : null;
}
/**
* Returns all existing local parts that match the search result.
* If no part is found, return an empty array.
* @param SearchResultDTO $dto
* @return Part[]
*/
public function findAllExisting(SearchResultDTO $dto): array
{
$qb = $this->em->getRepository(Part::class)->createQueryBuilder('part');
$qb->select('part')
->leftJoin('part.manufacturer', 'manufacturer')
->Orwhere($qb->expr()->andX(
'part.providerReference.provider_key = :providerKey',
'part.providerReference.provider_id = :providerId',
))
//Or the manufacturer (allowing for alternative names) and the MPN (or part name) must match
->OrWhere(
$qb->expr()->andX(
$qb->expr()->orX(
"ILIKE(manufacturer.name, :manufacturerName) = TRUE",
"ILIKE(manufacturer.alternative_names, :manufacturerAltNames) = TRUE",
),
$qb->expr()->orX(
"ILIKE(part.manufacturer_product_number, :mpn) = TRUE",
"ILIKE(part.name, :mpn) = TRUE",
)
)
)
;
$qb->setParameter('providerKey', $dto->provider_key);
$qb->setParameter('providerId', $dto->provider_id);
$qb->setParameter('manufacturerName', $dto->manufacturer);
$qb->setParameter('manufacturerAltNames', '%'.$dto->manufacturer.'%');
$qb->setParameter('mpn', $dto->mpn);
return $qb->getQuery()->getResult();
}
}

View file

@ -45,6 +45,14 @@ class DigikeyProvider implements InfoProviderInterface
private readonly HttpClientInterface $digikeyClient;
/**
* A list of parameter IDs, that are always assumed as text only and will never be converted to a numerical value.
* This allows to fix issues like #682, where the "Supplier Device Package" was parsed as a numerical value.
*/
private const TEXT_ONLY_PARAMETERS = [
1291, //Supplier Device Package
39246, //Package / Case
];
public function __construct(HttpClientInterface $httpClient, private readonly OAuthTokenManager $authTokenManager,
private readonly string $currency, private readonly string $clientId,
@ -214,7 +222,12 @@ class DigikeyProvider implements InfoProviderInterface
continue;
}
$results[] = ParameterDTO::parseValueIncludingUnit($parameter['Parameter'], $parameter['Value']);
//If the parameter was marked as text only, then we do not try to parse it as a numerical value
if (in_array($parameter['ParameterId'], self::TEXT_ONLY_PARAMETERS, true)) {
$results[] = new ParameterDTO(name: $parameter['Parameter'], value_text: $parameter['Value']);
} else { //Otherwise try to parse it as a numerical value
$results[] = ParameterDTO::parseValueIncludingUnit($parameter['Parameter'], $parameter['Value']);
}
}
return $results;

View file

@ -98,16 +98,19 @@ class LCSCProvider implements InfoProviderInterface
private function getRealDatasheetUrl(?string $url): string
{
if ($url !== null && trim($url) !== '' && preg_match("/^https:\/\/(datasheet\.lcsc\.com|www\.lcsc\.com\/datasheet)\/.*(C\d+)\.pdf$/", $url, $matches) > 0) {
if (preg_match("/^https:\/\/datasheet\.lcsc\.com\/lcsc\/(.*\.pdf)$/", $url, $rewriteMatches) > 0) {
$url = 'https://www.lcsc.com/datasheet/lcsc_datasheet_' . $rewriteMatches[1];
}
$response = $this->lcscClient->request('GET', $url, [
'headers' => [
'Referer' => 'https://www.lcsc.com/product-detail/_' . $matches[2] . '.html'
],
]);
if (preg_match('/(pdfUrl): ?("[^"]+wmsc\.lcsc\.com[^"]+\.pdf")/', $response->getContent(), $matches) > 0) {
if (preg_match('/(previewPdfUrl): ?("[^"]+wmsc\.lcsc\.com[^"]+\.pdf")/', $response->getContent(), $matches) > 0) {
//HACKY: The URL string contains escaped characters like \u002F, etc. To decode it, the JSON decoding is reused
//See https://github.com/Part-DB/Part-DB-server/pull/582#issuecomment-2033125934
$jsonObj = json_decode('{"' . $matches[1] . '": ' . $matches[2] . '}');
$url = $jsonObj->pdfUrl;
$url = $jsonObj->previewPdfUrl;
}
}
return $url;

View file

@ -203,7 +203,7 @@ class MouserProvider implements InfoProviderInterface
if (isset($arr['SearchResults'])) {
$products = $arr['SearchResults']['Parts'] ?? [];
} else {
throw new \RuntimeException('Unknown response format');
throw new \RuntimeException('Unknown response format: ' .json_encode($arr, JSON_THROW_ON_ERROR));
}
$result = [];

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,166 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 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);
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 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/>.
*/
namespace App\Services\LabelSystem\BarcodeScanner;
use App\Entity\LabelSystem\LabelSupportedElement;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityNotFoundException;
use InvalidArgumentException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeRedirectorTest
*/
final class BarcodeRedirector
{
public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em)
{
}
/**
* Determines the URL to which the user should be redirected, when scanning a QR code.
*
* @param BarcodeScanResultInterface $barcodeScan The result of the barcode scan
* @return string the URL to which should be redirected
*
* @throws EntityNotFoundException
*/
public function getRedirectURL(BarcodeScanResultInterface $barcodeScan): string
{
if($barcodeScan instanceof LocalBarcodeScanResult) {
return $this->getURLLocalBarcode($barcodeScan);
}
if ($barcodeScan instanceof EIGP114BarcodeScanResult) {
return $this->getURLVendorBarcode($barcodeScan);
}
throw new InvalidArgumentException('Unknown $barcodeScan type: '.get_class($barcodeScan));
}
private function getURLLocalBarcode(LocalBarcodeScanResult $barcodeScan): string
{
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, $barcodeScan->target_id);
if (!$lot instanceof PartLot) {
throw new EntityNotFoundException();
}
return $this->urlGenerator->generate('app_part_show', ['id' => $lot->getPart()->getID()]);
case LabelSupportedElement::STORELOCATION:
return $this->urlGenerator->generate('part_list_store_location', ['id' => $barcodeScan->target_id]);
default:
throw new InvalidArgumentException('Unknown $type: '.$barcodeScan->target_type->name);
}
}
/**
* Gets the URL to a part from a scan of a Vendor Barcode
*/
private function getURLVendorBarcode(EIGP114BarcodeScanResult $barcodeScan): string
{
$part = $this->getPartFromVendor($barcodeScan);
return $this->urlGenerator->generate('app_part_show', ['id' => $part->getID()]);
}
/**
* Gets a part from a scan of a Vendor Barcode by filtering for parts
* with the same Info Provider Id or, if that fails, by looking for parts with a
* matching manufacturer product number. Only returns the first matching part.
*/
private function getPartFromVendor(EIGP114BarcodeScanResult $barcodeScan) : Part
{
// first check via the info provider ID (e.g. Vendor ID). This might fail if the part was not added via
// the info provider system or if the part was bought from a different vendor than the data was retrieved
// from.
if($barcodeScan->digikeyPartNumber) {
$qb = $this->em->getRepository(Part::class)->createQueryBuilder('part');
//Lower() to be case insensitive
$qb->where($qb->expr()->like('LOWER(part.providerReference.provider_id)', 'LOWER(:vendor_id)'));
$qb->setParameter('vendor_id', $barcodeScan->digikeyPartNumber);
$results = $qb->getQuery()->getResult();
if ($results) {
return $results[0];
}
}
if(!$barcodeScan->supplierPartNumber){
throw new EntityNotFoundException();
}
//Fallback to the manufacturer part number. This may return false positives, since it is common for
//multiple manufacturers to use the same part number for their version of a common product
//We assume the user is able to realize when this returns the wrong part
//If the barcode specifies the manufacturer we try to use that as well
$mpnQb = $this->em->getRepository(Part::class)->createQueryBuilder('part');
$mpnQb->where($mpnQb->expr()->like('LOWER(part.manufacturer_product_number)', 'LOWER(:mpn)'));
$mpnQb->setParameter('mpn', $barcodeScan->supplierPartNumber);
if($barcodeScan->mouserManufacturer){
$manufacturerQb = $this->em->getRepository(Manufacturer::class)->createQueryBuilder("manufacturer");
$manufacturerQb->where($manufacturerQb->expr()->like("LOWER(manufacturer.name)", "LOWER(:manufacturer_name)"));
$manufacturerQb->setParameter("manufacturer_name", $barcodeScan->mouserManufacturer);
$manufacturers = $manufacturerQb->getQuery()->getResult();
if($manufacturers) {
$mpnQb->andWhere($mpnQb->expr()->eq("part.manufacturer", ":manufacturer"));
$mpnQb->setParameter("manufacturer", $manufacturers);
}
}
$results = $mpnQb->getQuery()->getResult();
if($results){
return $results[0];
}
throw new EntityNotFoundException();
}
}

View file

@ -39,7 +39,7 @@ declare(strict_types=1);
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\Barcodes;
namespace App\Services\LabelSystem\BarcodeScanner;
use App\Entity\LabelSystem\LabelSupportedElement;
use App\Entity\Parts\Part;
@ -75,20 +75,23 @@ final class BarcodeScanHelper
* will try to guess the type.
* @param string $input
* @param BarcodeSourceType|null $type
* @return BarcodeScanResult
* @return BarcodeScanResultInterface
*/
public function scanBarcodeContent(string $input, ?BarcodeSourceType $type = null): BarcodeScanResult
public function scanBarcodeContent(string $input, ?BarcodeSourceType $type = null): BarcodeScanResultInterface
{
//Do specific parsing
if ($type === BarcodeSourceType::INTERNAL) {
return $this->parseInternalBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode');
}
if ($type === BarcodeSourceType::VENDOR) {
return $this->parseVendorBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode');
if ($type === BarcodeSourceType::USER_DEFINED) {
return $this->parseUserDefinedBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode');
}
if ($type === BarcodeSourceType::IPN) {
return $this->parseIPNBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode');
}
if ($type === BarcodeSourceType::EIGP114) {
return $this->parseEIGP114Barcode($input);
}
//Null means auto and we try the different formats
$result = $this->parseInternalBarcode($input);
@ -97,12 +100,17 @@ final class BarcodeScanHelper
return $result;
}
//Try to parse as vendor barcode
$result = $this->parseVendorBarcode($input);
//Try to parse as User defined barcode
$result = $this->parseUserDefinedBarcode($input);
if ($result !== null) {
return $result;
}
//If the barcode is formatted as EIGP114, we can parse it directly
if (EIGP114BarcodeScanResult::isFormat06Code($input)) {
return $this->parseEIGP114Barcode($input);
}
//Try to parse as IPN barcode
$result = $this->parseIPNBarcode($input);
if ($result !== null) {
@ -112,11 +120,16 @@ final class BarcodeScanHelper
throw new InvalidArgumentException('Unknown barcode');
}
private function parseVendorBarcode(string $input): ?BarcodeScanResult
private function parseEIGP114Barcode(string $input): EIGP114BarcodeScanResult
{
return EIGP114BarcodeScanResult::parseFormat06Code($input);
}
private function parseUserDefinedBarcode(string $input): ?LocalBarcodeScanResult
{
$lot_repo = $this->entityManager->getRepository(PartLot::class);
//Find only the first result
$results = $lot_repo->findBy(['vendor_barcode' => $input], limit: 1);
$results = $lot_repo->findBy(['user_barcode' => $input], limit: 1);
if (count($results) === 0) {
return null;
@ -124,14 +137,14 @@ final class BarcodeScanHelper
//We found a part, so use it to create the result
$lot = $results[0];
return new BarcodeScanResult(
return new LocalBarcodeScanResult(
target_type: LabelSupportedElement::PART_LOT,
target_id: $lot->getID(),
source_type: BarcodeSourceType::VENDOR
source_type: BarcodeSourceType::USER_DEFINED
);
}
private function parseIPNBarcode(string $input): ?BarcodeScanResult
private function parseIPNBarcode(string $input): ?LocalBarcodeScanResult
{
$part_repo = $this->entityManager->getRepository(Part::class);
//Find only the first result
@ -143,7 +156,7 @@ final class BarcodeScanHelper
//We found a part, so use it to create the result
$part = $results[0];
return new BarcodeScanResult(
return new LocalBarcodeScanResult(
target_type: LabelSupportedElement::PART,
target_id: $part->getID(),
source_type: BarcodeSourceType::IPN
@ -155,9 +168,9 @@ final class BarcodeScanHelper
* 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
* @return LocalBarcodeScanResult|null
*/
private function parseInternalBarcode(string $input): ?BarcodeScanResult
private function parseInternalBarcode(string $input): ?LocalBarcodeScanResult
{
$input = trim($input);
$matches = [];
@ -167,7 +180,7 @@ final class BarcodeScanHelper
//Extract parts from QR code's URL
if (preg_match('#^https?://.*/scan/(\w+)/(\d+)/?$#', $input, $matches)) {
return new BarcodeScanResult(
return new LocalBarcodeScanResult(
target_type: self::QR_TYPE_MAP[strtolower($matches[1])],
target_id: (int) $matches[2],
source_type: BarcodeSourceType::INTERNAL
@ -183,7 +196,7 @@ final class BarcodeScanHelper
throw new InvalidArgumentException('Unknown prefix '.$prefix);
}
return new BarcodeScanResult(
return new LocalBarcodeScanResult(
target_type: self::PREFIX_TYPE_MAP[$prefix],
target_id: $id,
source_type: BarcodeSourceType::INTERNAL
@ -199,7 +212,7 @@ final class BarcodeScanHelper
throw new InvalidArgumentException('Unknown prefix '.$prefix);
}
return new BarcodeScanResult(
return new LocalBarcodeScanResult(
target_type: self::PREFIX_TYPE_MAP[$prefix],
target_id: $id,
source_type: BarcodeSourceType::INTERNAL
@ -208,7 +221,7 @@ final class BarcodeScanHelper
//Legacy Part-DB location labels used $L00336 format
if (preg_match('#^\$L(\d{5,})$#', $input, $matches)) {
return new BarcodeScanResult(
return new LocalBarcodeScanResult(
target_type: LabelSupportedElement::STORELOCATION,
target_id: (int) $matches[1],
source_type: BarcodeSourceType::INTERNAL
@ -217,7 +230,7 @@ final class BarcodeScanHelper
//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 new BarcodeScanResult(
return new LocalBarcodeScanResult(
target_type: LabelSupportedElement::PART,
target_id: (int) $matches[1],
source_type: BarcodeSourceType::INTERNAL

View file

@ -0,0 +1,36 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 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\LabelSystem\BarcodeScanner;
interface BarcodeScanResultInterface
{
/**
* Returns all data that was decoded from the barcode in a format, that can be shown in a table to the user.
* The return values of this function are not meant to be parsed by code again, but should just give a information
* to the user.
* The keys of the returned array are the first column of the table and the values are the second column.
* @return array<string, string|int|float|null>
*/
public function getDecodedForInfoMode(): array;
}

View file

@ -21,7 +21,7 @@
declare(strict_types=1);
namespace App\Services\LabelSystem\Barcodes;
namespace App\Services\LabelSystem\BarcodeScanner;
/**
* This enum represents the different types, where a barcode/QR-code can be generated from
@ -32,9 +32,14 @@ enum BarcodeSourceType
case INTERNAL;
/** This barcode is containing an internal part number (IPN) */
case IPN;
/**
* This barcode is a custom barcode from a third party like a vendor, which was set via the vendor_barcode
* field of a part lot.
* This barcode is a user defined barcode defined on a part lot
*/
case VENDOR;
case USER_DEFINED;
/**
* EIGP114 formatted barcodes like used by digikey, mouser, etc.
*/
case EIGP114;
}

View file

@ -0,0 +1,332 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 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\LabelSystem\BarcodeScanner;
/**
* This class represents the content of a EIGP114 barcode.
* Based on PR 811, EIGP 114.2018 (https://www.ecianow.org/assets/docs/GIPC/EIGP-114.2018%20ECIA%20Labeling%20Specification%20for%20Product%20and%20Shipment%20Identification%20in%20the%20Electronics%20Industry%20-%202D%20Barcode.pdf),
* , https://forum.digikey.com/t/digikey-product-labels-decoding-digikey-barcodes/41097
*/
class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
{
/**
* @var string|null Ship date in format YYYYMMDD
*/
public readonly ?string $shipDate;
/**
* @var string|null Customer assigned part number Optional based on
* agreements between Distributor and Supplier
*/
public readonly ?string $customerPartNumber;
/**
* @var string|null Supplier assigned part number
*/
public readonly ?string $supplierPartNumber;
/**
* @var int|null Quantity of product
*/
public readonly ?int $quantity;
/**
* @var string|null Customer assigned purchase order number
*/
public readonly ?string $customerPO;
/**
* @var string|null Line item number from PO. Required on Logistic Label when
* used on back of Packing Slip. See Section 4.9
*/
public readonly ?string $customerPOLine;
/**
* 9D - YYWW (Year and Week of Manufacture). ) If no date code is used
* for a particular part, this field should be populated with N/T
* to indicate the product is Not Traceable by this data field.
* @var string|null
*/
public readonly ?string $dateCode;
/**
* 10D - YYWW (Year and Week of Manufacture). ) If no date code is used
* for a particular part, this field should be populated with N/T
* to indicate the product is Not Traceable by this data field.
* @var string|null
*/
public readonly ?string $alternativeDateCode;
/**
* Traceability number assigned to a batch or group of items. If
* no lot code is used for a particular part, this field should be
* populated with N/T to indicate the product is Not Traceable
* by this data field.
* @var string|null
*/
public readonly ?string $lotCode;
/**
* Country where part was manufactured. Two-letter code from
* ISO 3166 country code list
* @var string|null
*/
public readonly ?string $countryOfOrigin;
/**
* @var string|null Unique alphanumeric number assigned by supplier
* 3S - Package ID for Inner Pack when part of a mixed Logistic
* Carton. Always used in conjunction with a mixed logistic label
* with a 5S data identifier for Package ID.
*/
public readonly ?string $packageId1;
/**
* @var string|null
* 4S - Package ID for Logistic Carton with like items
*/
public readonly ?string $packageId2;
/**
* @var string|null
* 5S - Package ID for Logistic Carton with mixed items
*/
public readonly ?string $packageId3;
/**
* @var string|null Unique alphanumeric number assigned by supplier.
*/
public readonly ?string $packingListNumber;
/**
* @var string|null Ship date in format YYYYMMDD
*/
public readonly ?string $serialNumber;
/**
* @var string|null Code for sorting and classifying LEDs. Use when applicable
*/
public readonly ?string $binCode;
/**
* @var int|null Sequential carton count in format #/#” or “# of #”
*/
public readonly ?int $packageCount;
/**
* @var string|null Alphanumeric string assigned by the supplier to distinguish
* from one closely-related design variation to another. Use as
* required or when applicable
*/
public readonly ?string $revisionNumber;
/**
* @var string|null Digikey Extension: This is not represented in the ECIA spec, but the field being used is found in the ANSI MH10.8.2-2016 spec on which the ECIA spec is based. In the ANSI spec it is called First Level (Supplier Assigned) Part Number.
*/
public readonly ?string $digikeyPartNumber;
/**
* @var string|null Digikey Extension: This can be shared across multiple invoices and time periods and is generated as an order enters our system from any vector (web, API, phone order, etc.)
*/
public readonly ?string $digikeySalesOrderNumber;
/**
* @var string|null Digikey extension: This is typically assigned per shipment as items are being released to be picked in the warehouse. A SO can have many Invoice numbers
*/
public readonly ?string $digikeyInvoiceNumber;
/**
* @var string|null Digikey extension: This is for internal DigiKey purposes and defines the label type.
*/
public readonly ?string $digikeyLabelType;
/**
* @var string|null You will also see this as the last part of a URL for a product detail page. Ex https://www.digikey.com/en/products/detail/w%C3%BCrth-elektronik/860010672008/5726907
*/
public readonly ?string $digikeyPartID;
/**
* @var string|null Digikey Extension: For internal use of Digikey. Probably not needed
*/
public readonly ?string $digikeyNA;
/**
* @var string|null Digikey Extension: This is a field of varying length used to keep the barcode approximately the same size between labels. It is safe to ignore.
*/
public readonly ?string $digikeyPadding;
public readonly ?string $mouserPositionInOrder;
public readonly ?string $mouserManufacturer;
/**
*
* @param array<string, string> $data The fields of the EIGP114 barcode, where the key is the field name and the value is the field content
*/
public function __construct(public readonly array $data)
{
//IDs per EIGP 114.2018
$this->shipDate = $data['6D'] ?? null;
$this->customerPartNumber = $data['P'] ?? null;
$this->supplierPartNumber = $data['1P'] ?? null;
$this->quantity = isset($data['Q']) ? (int)$data['Q'] : null;
$this->customerPO = $data['K'] ?? null;
$this->customerPOLine = $data['4K'] ?? null;
$this->dateCode = $data['9D'] ?? null;
$this->alternativeDateCode = $data['10D'] ?? null;
$this->lotCode = $data['1T'] ?? null;
$this->countryOfOrigin = $data['4L'] ?? null;
$this->packageId1 = $data['3S'] ?? null;
$this->packageId2 = $data['4S'] ?? null;
$this->packageId3 = $data['5S'] ?? null;
$this->packingListNumber = $data['11K'] ?? null;
$this->serialNumber = $data['S'] ?? null;
$this->binCode = $data['33P'] ?? null;
$this->packageCount = isset($data['13Q']) ? (int)$data['13Q'] : null;
$this->revisionNumber = $data['2P'] ?? null;
//IDs used by Digikey
$this->digikeyPartNumber = $data['30P'] ?? null;
$this->digikeySalesOrderNumber = $data['1K'] ?? null;
$this->digikeyInvoiceNumber = $data['10K'] ?? null;
$this->digikeyLabelType = $data['11Z'] ?? null;
$this->digikeyPartID = $data['12Z'] ?? null;
$this->digikeyNA = $data['13Z'] ?? null;
$this->digikeyPadding = $data['20Z'] ?? null;
//IDs used by Mouser
$this->mouserPositionInOrder = $data['14K'] ?? null;
$this->mouserManufacturer = $data['1V'] ?? null;
}
/**
* Tries to guess the vendor of the barcode based on the supplied data field.
* This is experimental and should not be relied upon.
* @return string|null The guessed vendor as smallcase string (e.g. "digikey", "mouser", etc.), or null if the vendor could not be guessed
*/
public function guessBarcodeVendor(): ?string
{
//If the barcode data contains the digikey extensions, we assume it is a digikey barcode
if (isset($this->data['13Z']) || isset($this->data['20Z']) || isset($this->data['12Z']) || isset($this->data['11Z'])) {
return 'digikey';
}
//If the barcode data contains the mouser extensions, we assume it is a mouser barcode
if (isset($this->data['14K']) || isset($this->data['1V'])) {
return 'mouser';
}
//According to this thread (https://github.com/inventree/InvenTree/issues/853), Newark/element14 codes contains a "3P" field
if (isset($this->data['3P'])) {
return 'element14';
}
return null;
}
/**
* Checks if the given input is a valid format06 formatted data.
* This just perform a simple check for the header, the content might be malformed still.
* @param string $input
* @return bool
*/
public static function isFormat06Code(string $input): bool
{
//Code must begin with [)><RS>06<GS>
if(!str_starts_with($input, "[)>\u{1E}06\u{1D}")){
return false;
}
//Digikey does not put a trailer onto the barcode, so we just check for the header
return true;
}
/**
* Parses a format06 code a returns a new instance of this class
* @param string $input
* @return self
*/
public static function parseFormat06Code(string $input): self
{
//Ensure that the input is a valid format06 code
if (!self::isFormat06Code($input)) {
throw new \InvalidArgumentException("The given input is not a valid format06 code");
}
//Remove the trailer, if present
if (str_ends_with($input, "\u{1E}\u{04}")){
$input = substr($input, 5, -2);
}
//Split the input into the different fields (using the <GS> separator)
$parts = explode("\u{1D}", $input);
//The first field is the format identifier, which we do not need
array_shift($parts);
//Split the fields into key-value pairs
$results = [];
foreach($parts as $part) {
//^ 0* ([1-9]? \d* [A-Z])
//Start of the string Leading zeros are discarded Not a zero Any number of digits single uppercase Letter
// 00 1 4 K
if(!preg_match('/^0*([1-9]?\d*[A-Z])/', $part, $matches)) {
throw new \LogicException("Could not parse field: $part");
}
//Extract the key
$key = $matches[0];
//Extract the field value
$fieldValue = substr($part, strlen($matches[0]));
$results[$key] = $fieldValue;
}
return new self($results);
}
public function getDecodedForInfoMode(): array
{
$tmp = [
'Barcode type' => 'EIGP114',
'Guessed vendor from barcode' => $this->guessBarcodeVendor() ?? 'Unknown',
];
//Iterate over all fields of this object and add them to the array if they are not null
foreach((array) $this as $key => $value) {
//Skip data key
if ($key === 'data') {
continue;
}
if($value !== null) {
$tmp[$key] = $value;
}
}
return $tmp;
}
}

View file

@ -21,14 +21,15 @@
declare(strict_types=1);
namespace App\Services\LabelSystem\Barcodes;
namespace App\Services\LabelSystem\BarcodeScanner;
use App\Entity\LabelSystem\LabelSupportedElement;
/**
* This class represents the result of a barcode scan, with the target type and the ID of the element
* This class represents the result of a barcode scan of a barcode that uniquely identifies a local entity,
* like an internally generated barcode or a barcode that was added manually to the system by a user
*/
class BarcodeScanResult
class LocalBarcodeScanResult implements BarcodeScanResultInterface
{
public function __construct(
public readonly LabelSupportedElement $target_type,
@ -36,4 +37,13 @@ class BarcodeScanResult
public readonly BarcodeSourceType $source_type,
) {
}
public function getDecodedForInfoMode(): array
{
return [
'Barcode type' => $this->source_type->name,
'Target type' => $this->target_type->name,
'Target ID' => $this->target_id,
];
}
}

View file

@ -1,89 +0,0 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 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);
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 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/>.
*/
namespace App\Services\LabelSystem\Barcodes;
use App\Entity\LabelSystem\LabelSupportedElement;
use App\Entity\Parts\PartLot;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityNotFoundException;
use InvalidArgumentException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeRedirectorTest
*/
final class BarcodeRedirector
{
public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em)
{
}
/**
* Determines the URL to which the user should be redirected, when scanning a QR code.
*
* @param BarcodeScanResult $barcodeScan The result of the barcode scan
* @return string the URL to which should be redirected
*
* @throws EntityNotFoundException
*/
public function getRedirectURL(BarcodeScanResult $barcodeScan): string
{
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, $barcodeScan->target_id);
if (!$lot instanceof PartLot) {
throw new EntityNotFoundException();
}
return $this->urlGenerator->generate('app_part_show', ['id' => $lot->getPart()->getID()]);
case LabelSupportedElement::STORELOCATION:
return $this->urlGenerator->generate('part_list_store_location', ['id' => $barcodeScan->target_id]);
default:
throw new InvalidArgumentException('Unknown $type: '.$barcodeScan->target_type->name);
}
}
}

View file

@ -146,11 +146,11 @@ final class LabelExampleElementsGenerator
throw new InvalidArgumentException('$class must be an child of AbstractStructuralDBElement');
}
/** @var AbstractStructuralDBElement $parent */
/** @var T $parent */
$parent = new $class();
$parent->setName('Example');
/** @var AbstractStructuralDBElement $child */
/** @var T $child */
$child = new $class();
$child->setName((new ReflectionClass($class))->getShortName());
$child->setParent($parent);

View file

@ -62,10 +62,6 @@ final class LabelGenerator
*/
public function generateLabel(LabelOptions $options, object|array $elements): string
{
if (!is_array($elements) && !is_object($elements)) {
throw new InvalidArgumentException('$element must be an object or an array of objects!');
}
if (!is_array($elements)) {
$elements = [$elements];
}

View file

@ -74,8 +74,7 @@ final class LabelProfileDropdownHelper
$secure_class_name = $this->tagGenerator->getElementTypeCacheTag(LabelProfile::class);
$key = 'profile_dropdown_'.$this->keyGenerator->generateKey().'_'.$secure_class_name.'_'.$type->value;
/** @var LabelProfileRepository $repo */
$repo = $this->entityManager->getRepository(LabelProfile::class);
return $this->cache->get($key, function (ItemInterface $item) use ($repo, $type, $secure_class_name) {

View file

@ -71,6 +71,7 @@ use App\Twig\TwigCoreExtension;
use InvalidArgumentException;
use Twig\Environment;
use Twig\Extension\SandboxExtension;
use Twig\Extra\Html\HtmlExtension;
use Twig\Extra\Intl\IntlExtension;
use Twig\Extra\Markdown\MarkdownExtension;
use Twig\Extra\String\StringExtension;
@ -183,6 +184,7 @@ final class SandboxedTwigFactory
$twig->addExtension(new IntlExtension());
$twig->addExtension(new MarkdownExtension());
$twig->addExtension(new StringExtension());
$twig->addExtension(new HtmlExtension());
//Add Part-DB specific extension
$twig->addExtension($this->formatExtension);

View file

@ -216,7 +216,10 @@ class TimeTravel
$old_data = $logEntry->getOldData();
foreach ($old_data as $field => $data) {
if ($metadata->hasField($field)) {
//We use the fieldMappings property directly instead of the hasField method, as we do not want to match the embedded field itself
//The sub fields are handled in the setField method
if (isset($metadata->fieldMappings[$field])) {
//We need to convert the string to a BigDecimal first
if (!$data instanceof BigDecimal && ('big_decimal' === $metadata->getFieldMapping($field)->type)) {
$data = BigDecimal::of($data);
@ -224,7 +227,12 @@ class TimeTravel
if (!$data instanceof \DateTimeInterface
&& (in_array($metadata->getFieldMapping($field)->type,
[Types::DATETIME_IMMUTABLE, Types::DATETIME_IMMUTABLE, Types::DATE_MUTABLE, Types::DATETIME_IMMUTABLE], true))) {
[
Types::DATETIME_IMMUTABLE,
Types::DATETIME_IMMUTABLE,
Types::DATE_MUTABLE,
Types::DATETIME_IMMUTABLE
], true))) {
$data = $this->dateTimeDecode($data, $metadata->getFieldMapping($field)->type);
}
@ -267,9 +275,11 @@ class TimeTravel
$embeddedReflection = new ReflectionClass($embeddedClass::class);
$property = $embeddedReflection->getProperty($embedded_field);
$target_element = $embeddedClass;
} else {
$reflection = new ReflectionClass($element::class);
$property = $reflection->getProperty($field);
$target_element = $element;
}
//Check if the property is an BackedEnum, then convert the int or float value to an enum instance
@ -281,6 +291,6 @@ class TimeTravel
$new_value = $enum_class::from($new_value);
}
$property->setValue($element, $new_value);
$property->setValue($target_element, $new_value);
}
}

View file

@ -53,7 +53,6 @@ final class PartsTableActionHandler
{
$id_array = explode(',', $ids);
/** @var PartRepository $repo */
$repo = $this->entityManager->getRepository(Part::class);
return $repo->getElementsFromIDArray($id_array);

View file

@ -122,7 +122,6 @@ class StatisticsHelper
throw new InvalidArgumentException('No count for the given type available!');
}
/** @var EntityRepository $repo */
$repo = $this->em->getRepository($arr[$type]);
return $repo->count([]);

View file

@ -66,7 +66,7 @@ class TagFinder
$qb->select('p.tags')
->from(Part::class, 'p')
->where('p.tags LIKE ?1')
->where('ILIKE(p.tags, ?1) = TRUE')
->setMaxResults($options['query_limit'])
//->orderBy('RAND()')
->setParameter(1, '%'.$keyword.'%');

View file

@ -23,9 +23,11 @@ declare(strict_types=1);
namespace App\Services\Trees;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Repository\AttachmentContainingDBElementRepository;
use App\Repository\DBElementRepository;
use App\Repository\NamedDBElementRepository;
use App\Repository\StructuralDBElementRepository;
use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Cache\UserCacheKeyGenerator;
@ -51,7 +53,7 @@ class NodesListBuilder
* Gets a flattened hierarchical tree. Useful for generating option lists.
* In difference to the Repository Function, the results here are cached.
*
* @template T of AbstractDBElement
* @template T of AbstractNamedDBElement
*
* @param string $class_name the class name of the entity you want to retrieve
* @phpstan-param class-string<T> $class_name
@ -69,7 +71,7 @@ class NodesListBuilder
$ids = $this->getFlattenedIDs($class_name, $parent);
//Retrieve the elements from the IDs, the order is the same as in the $ids array
/** @var DBElementRepository $repo */
/** @var NamedDBElementRepository<T> $repo */
$repo = $this->em->getRepository($class_name);
if ($repo instanceof AttachmentContainingDBElementRepository) {
@ -81,7 +83,9 @@ class NodesListBuilder
/**
* This functions returns the (cached) list of the IDs of the elements for the flattened tree.
* @template T of AbstractNamedDBElement
* @param string $class_name
* @phpstan-param class-string<T> $class_name
* @param AbstractStructuralDBElement|null $parent
* @return int[]
*/
@ -96,10 +100,12 @@ class NodesListBuilder
// Invalidate when groups, an element with the class or the user changes
$item->tag(['groups', 'tree_list', $this->keyGenerator->generateKey(), $secure_class_name]);
/** @var StructuralDBElementRepository $repo */
/** @var NamedDBElementRepository<T> $repo */
$repo = $this->em->getRepository($class_name);
return array_map(static fn(AbstractDBElement $element) => $element->getID(), $repo->getFlatList($parent));
return array_map(static fn(AbstractDBElement $element) => $element->getID(),
//@phpstan-ignore-next-line For some reason phpstan does not understand that $repo is a StructuralDBElementRepository
$repo->getFlatList($parent));
});
}

View file

@ -33,6 +33,7 @@ use App\Entity\Parts\Supplier;
use App\Entity\ProjectSystem\Project;
use App\Helpers\Trees\TreeViewNode;
use App\Helpers\Trees\TreeViewNodeIterator;
use App\Repository\NamedDBElementRepository;
use App\Repository\StructuralDBElementRepository;
use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Cache\UserCacheKeyGenerator;
@ -224,6 +225,7 @@ class TreeViewGenerator
* The treeview is generic, that means the href are null and ID values are set.
*
* @param string $class The class for which the tree should be generated
* @phpstan-param class-string<AbstractNamedDBElement> $class
* @param AbstractStructuralDBElement|null $parent the parent the root elements should have
*
* @return TreeViewNode[]
@ -237,12 +239,12 @@ class TreeViewGenerator
throw new InvalidArgumentException('$parent must be of the type $class!');
}
/** @var StructuralDBElementRepository $repo */
/** @var NamedDBElementRepository<AbstractNamedDBElement> $repo */
$repo = $this->em->getRepository($class);
//If we just want a part of a tree, don't cache it
if ($parent instanceof AbstractStructuralDBElement) {
return $repo->getGenericNodeTree($parent);
return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line PHPstan does not seem to recognize, that we have a StructuralDBElementRepository here, which have 1 argument
}
$secure_class_name = $this->tagGenerator->getElementTypeCacheTag($class);
@ -251,7 +253,7 @@ class TreeViewGenerator
return $this->cache->get($key, function (ItemInterface $item) use ($repo, $parent, $secure_class_name) {
// Invalidate when groups, an element with the class or the user changes
$item->tag(['groups', 'tree_treeview', $this->keyGenerator->generateKey(), $secure_class_name]);
return $repo->getGenericNodeTree($parent);
return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line
});
}
}