mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-01-20 09:09:33 +00:00
Merge branch 'master' into SplitAttachmentPaths
This commit is contained in:
commit
524900c34a
55 changed files with 5821 additions and 1805 deletions
|
|
@ -43,9 +43,9 @@ class AddDocumentedAPIPropertiesJSONSchemaFactory implements SchemaFactoryInterf
|
|||
string $className,
|
||||
string $format = 'json',
|
||||
string $type = Schema::TYPE_OUTPUT,
|
||||
Operation $operation = null,
|
||||
Schema $schema = null,
|
||||
array $serializerContext = null,
|
||||
?Operation $operation = null,
|
||||
?Schema $schema = null,
|
||||
?array $serializerContext = null,
|
||||
bool $forceCollection = false
|
||||
): Schema {
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class EntityFilter extends AbstractFilter
|
|||
public function __construct(
|
||||
ManagerRegistry $managerRegistry,
|
||||
private readonly EntityFilterHelper $filter_helper,
|
||||
LoggerInterface $logger = null,
|
||||
?LoggerInterface $logger = null,
|
||||
?array $properties = null,
|
||||
?NameConverterInterface $nameConverter = null
|
||||
) {
|
||||
|
|
@ -50,7 +50,7 @@ class EntityFilter extends AbstractFilter
|
|||
QueryBuilder $queryBuilder,
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
Operation $operation = null,
|
||||
?Operation $operation = null,
|
||||
array $context = []
|
||||
): void {
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ final class LikeFilter extends AbstractFilter
|
|||
QueryBuilder $queryBuilder,
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
Operation $operation = null,
|
||||
?Operation $operation = null,
|
||||
array $context = []
|
||||
): void {
|
||||
// Otherwise filter is applied to order and page as well
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class PartStoragelocationFilter extends AbstractFilter
|
|||
public function __construct(
|
||||
ManagerRegistry $managerRegistry,
|
||||
private readonly EntityFilterHelper $filter_helper,
|
||||
LoggerInterface $logger = null,
|
||||
?LoggerInterface $logger = null,
|
||||
?array $properties = null,
|
||||
?NameConverterInterface $nameConverter = null
|
||||
) {
|
||||
|
|
@ -51,7 +51,7 @@ class PartStoragelocationFilter extends AbstractFilter
|
|||
QueryBuilder $queryBuilder,
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
Operation $operation = null,
|
||||
?Operation $operation = null,
|
||||
array $context = []
|
||||
): void {
|
||||
//Do not check for mapping here, as we are using a virtual property
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
|||
#[AsCommand('partdb:users:enable|partdb:user:enable', 'Enables/Disable the login of one or more users')]
|
||||
class UserEnableCommand extends Command
|
||||
{
|
||||
public function __construct(protected EntityManagerInterface $entityManager, string $name = null)
|
||||
public function __construct(protected EntityManagerInterface $entityManager, ?string $name = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class TwoStepORMAdapter extends ORMAdapter
|
|||
|
||||
private \Closure|null $query_modifier = null;
|
||||
|
||||
public function __construct(ManagerRegistry $registry = null)
|
||||
public function __construct(?ManagerRegistry $registry = null)
|
||||
{
|
||||
parent::__construct($registry);
|
||||
$this->detailQueryCallable = static function (QueryBuilder $qb, array $ids): never {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ abstract class AbstractConstraint implements FilterInterface
|
|||
* @var string The property where this BooleanConstraint should apply to
|
||||
*/
|
||||
protected string $property,
|
||||
string $identifier = null)
|
||||
?string $identifier = null)
|
||||
{
|
||||
$this->identifier = $identifier ?? $this->generateParameterIdentifier($property);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class BooleanConstraint extends AbstractConstraint
|
|||
{
|
||||
public function __construct(
|
||||
string $property,
|
||||
string $identifier = null,
|
||||
?string $identifier = null,
|
||||
/** @var bool|null The value of our constraint */
|
||||
protected ?bool $value = null
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class DateTimeConstraint extends AbstractConstraint
|
|||
|
||||
public function __construct(
|
||||
string $property,
|
||||
string $identifier = null,
|
||||
?string $identifier = null,
|
||||
/**
|
||||
* The value1 used for comparison (this is the main one used for all mono-value comparisons)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class EntityConstraint extends AbstractConstraint
|
|||
public function __construct(protected ?NodesListBuilder $nodesListBuilder,
|
||||
protected string $class,
|
||||
string $property,
|
||||
string $identifier = null,
|
||||
?string $identifier = null,
|
||||
protected ?AbstractDBElement $value = null,
|
||||
protected ?string $operator = null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class NumberConstraint extends AbstractConstraint
|
|||
|
||||
public function __construct(
|
||||
string $property,
|
||||
string $identifier = null,
|
||||
?string $identifier = null,
|
||||
/**
|
||||
* The value1 used for comparison (this is the main one used for all mono-value comparisons)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ use Doctrine\ORM\QueryBuilder;
|
|||
|
||||
class LessThanDesiredConstraint extends BooleanConstraint
|
||||
{
|
||||
public function __construct(string $property = null, string $identifier = null, ?bool $default_value = null)
|
||||
public function __construct(?string $property = null, ?string $identifier = null, ?bool $default_value = null)
|
||||
{
|
||||
parent::__construct($property ?? '(
|
||||
SELECT COALESCE(SUM(ld_partLot.amount), 0.0)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class TagsConstraint extends AbstractConstraint
|
|||
{
|
||||
final public const ALLOWED_OPERATOR_VALUES = ['ANY', 'ALL', 'NONE'];
|
||||
|
||||
public function __construct(string $property, string $identifier = null,
|
||||
public function __construct(string $property, ?string $identifier = null,
|
||||
protected ?string $value = null,
|
||||
protected ?string $operator = '')
|
||||
{
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class TextConstraint extends AbstractConstraint
|
|||
/**
|
||||
* @param string $value
|
||||
*/
|
||||
public function __construct(string $property, string $identifier = null, /**
|
||||
public function __construct(string $property, ?string $identifier = null, /**
|
||||
* @var string|null The value to compare to
|
||||
*/
|
||||
protected ?string $value = null, /**
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
|
|||
*
|
||||
* @return string the link to the article
|
||||
*/
|
||||
public function getAutoProductUrl(string $partnr = null): string
|
||||
public function getAutoProductUrl(?string $partnr = null): string
|
||||
{
|
||||
if (is_string($partnr)) {
|
||||
return str_replace('%PARTNUMBER%', $partnr, $this->auto_product_url);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface
|
|||
*/
|
||||
private const DEFAULT_EXPIRATION_TIME = 3600;
|
||||
|
||||
public function __construct(string $name, ?string $refresh_token, ?string $token = null, \DateTimeImmutable $expires_at = null)
|
||||
public function __construct(string $name, ?string $refresh_token, ?string $token = null, ?\DateTimeImmutable $expires_at = null)
|
||||
{
|
||||
//If token is given, you also have to give the expires_at date
|
||||
if ($token !== null && $expires_at === null) {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ trait WithPermPresetsTrait
|
|||
return json_encode($user->getPermissions());
|
||||
}
|
||||
|
||||
public function setContainer(ContainerInterface $container = null): void
|
||||
public function setContainer(?ContainerInterface $container = null): void
|
||||
{
|
||||
if ($container !== null) {
|
||||
$this->container = $container;
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ class LogEntryRepository extends DBElementRepository
|
|||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
*/
|
||||
public function getLogsOrderedByTimestamp(string $order = 'DESC', int $limit = null, int $offset = null): array
|
||||
public function getLogsOrderedByTimestamp(string $order = 'DESC', ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->findBy([], ['timestamp' => $order], $limit, $offset);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ class ApiTokenAuthenticator implements AuthenticatorInterface
|
|||
/**
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-3
|
||||
*/
|
||||
private function getAuthenticateHeader(string $errorDescription = null): string
|
||||
private function getAuthenticateHeader(?string $errorDescription = null): string
|
||||
{
|
||||
$data = [
|
||||
'realm' => $this->realm,
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class AuthenticationEntryPoint implements AuthenticationEntryPointInterface
|
|||
) {
|
||||
}
|
||||
|
||||
public function start(Request $request, AuthenticationException $authException = null): Response
|
||||
public function start(Request $request, ?AuthenticationException $authException = null): Response
|
||||
{
|
||||
//Check if the request is an API request
|
||||
if ($this->isJSONRequest($request)) {
|
||||
|
|
|
|||
|
|
@ -116,10 +116,10 @@ class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterf
|
|||
* Maps a list of SAML roles to a local group ID.
|
||||
* The first available mapping will be used (so the order of the $map is important, first match wins).
|
||||
* @param array $roles The list of SAML roles
|
||||
* @param array $map|null The mapping from SAML roles. If null, the global mapping will be used.
|
||||
* @param array|null $map The mapping from SAML roles. If null, the global mapping will be used.
|
||||
* @return int|null The ID of the local group or null if no mapping was found.
|
||||
*/
|
||||
public function mapSAMLRolesToLocalGroupID(array $roles, array $map = null): ?int
|
||||
public function mapSAMLRolesToLocalGroupID(array $roles, ?array $map = null): ?int
|
||||
{
|
||||
$map ??= $this->saml_role_mapping;
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class AttachmentNormalizer implements NormalizerInterface, NormalizerAwareInterf
|
|||
{
|
||||
}
|
||||
|
||||
public function normalize(mixed $object, string $format = null, array $context = []): array|null
|
||||
public function normalize(mixed $object, ?string $format = null, array $context = []): array|null
|
||||
{
|
||||
if (!$object instanceof Attachment) {
|
||||
throw new \InvalidArgumentException('This normalizer only supports Attachment objects!');
|
||||
|
|
@ -64,7 +64,7 @@ class AttachmentNormalizer implements NormalizerInterface, NormalizerAwareInterf
|
|||
return $data;
|
||||
}
|
||||
|
||||
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
|
||||
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
|
||||
{
|
||||
// avoid recursion: only call once per object
|
||||
if (isset($context[self::ALREADY_CALLED])) {
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
|||
class BigNumberNormalizer implements NormalizerInterface, DenormalizerInterface
|
||||
{
|
||||
|
||||
public function supportsNormalization($data, string $format = null, array $context = []): bool
|
||||
public function supportsNormalization($data, ?string $format = null, array $context = []): bool
|
||||
{
|
||||
return $data instanceof BigNumber;
|
||||
}
|
||||
|
||||
public function normalize($object, string $format = null, array $context = []): string
|
||||
public function normalize($object, ?string $format = null, array $context = []): string
|
||||
{
|
||||
if (!$object instanceof BigNumber) {
|
||||
throw new \InvalidArgumentException('This normalizer only supports BigNumber objects!');
|
||||
|
|
@ -58,7 +58,7 @@ class BigNumberNormalizer implements NormalizerInterface, DenormalizerInterface
|
|||
];
|
||||
}
|
||||
|
||||
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): BigNumber|null
|
||||
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): BigNumber|null
|
||||
{
|
||||
if (!is_a($type, BigNumber::class, true)) {
|
||||
throw new \InvalidArgumentException('This normalizer only supports BigNumber objects!');
|
||||
|
|
@ -67,7 +67,7 @@ class BigNumberNormalizer implements NormalizerInterface, DenormalizerInterface
|
|||
return $type::of($data);
|
||||
}
|
||||
|
||||
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
|
||||
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
|
||||
{
|
||||
//data must be a string or a number (int, float, etc.) and the type must be BigNumber or BigDecimal
|
||||
return (is_string($data) || is_numeric($data)) && (is_subclass_of($type, BigNumber::class));
|
||||
|
|
|
|||
|
|
@ -63,13 +63,13 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Norm
|
|||
{
|
||||
}
|
||||
|
||||
public function supportsNormalization($data, string $format = null, array $context = []): bool
|
||||
public function supportsNormalization($data, ?string $format = null, array $context = []): bool
|
||||
{
|
||||
//We only remove the type field for CSV export
|
||||
return !isset($context[self::ALREADY_CALLED]) && $format === 'csv' && $data instanceof Part ;
|
||||
}
|
||||
|
||||
public function normalize($object, string $format = null, array $context = []): array
|
||||
public function normalize($object, ?string $format = null, array $context = []): array
|
||||
{
|
||||
if (!$object instanceof Part) {
|
||||
throw new \InvalidArgumentException('This normalizer only supports Part objects!');
|
||||
|
|
@ -117,7 +117,7 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Norm
|
|||
return $data;
|
||||
}
|
||||
|
||||
public function denormalize($data, string $type, string $format = null, array $context = []): ?Part
|
||||
public function denormalize($data, string $type, ?string $format = null, array $context = []): ?Part
|
||||
{
|
||||
$this->normalizeKeys($data);
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class StructuralElementDenormalizer implements DenormalizerInterface, Denormaliz
|
|||
{
|
||||
}
|
||||
|
||||
public function supportsDenormalization($data, string $type, string $format = null, array $context = []): bool
|
||||
public function supportsDenormalization($data, string $type, ?string $format = null, array $context = []): bool
|
||||
{
|
||||
//Only denormalize if we are doing a file import operation
|
||||
if (!($context['partdb_import'] ?? false)) {
|
||||
|
|
@ -78,7 +78,7 @@ class StructuralElementDenormalizer implements DenormalizerInterface, Denormaliz
|
|||
* @return AbstractStructuralDBElement|null
|
||||
* @phpstan-return T|null
|
||||
*/
|
||||
public function denormalize($data, string $type, string $format = null, array $context = []): ?AbstractStructuralDBElement
|
||||
public function denormalize($data, string $type, ?string $format = null, array $context = []): ?AbstractStructuralDBElement
|
||||
{
|
||||
//Do not use API Platform's denormalizer
|
||||
$context[SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER] = true;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class StructuralElementFromNameDenormalizer implements DenormalizerInterface
|
|||
{
|
||||
}
|
||||
|
||||
public function supportsDenormalization($data, string $type, string $format = null, array $context = []): bool
|
||||
public function supportsDenormalization($data, string $type, ?string $format = null, array $context = []): bool
|
||||
{
|
||||
//Only denormalize if we are doing a file import operation
|
||||
if (!($context['partdb_import'] ?? false)) {
|
||||
|
|
@ -51,7 +51,7 @@ class StructuralElementFromNameDenormalizer implements DenormalizerInterface
|
|||
* @phpstan-param class-string<T> $type
|
||||
* @phpstan-return T|null
|
||||
*/
|
||||
public function denormalize($data, string $type, string $format = null, array $context = []): AbstractStructuralDBElement|null
|
||||
public function denormalize($data, string $type, ?string $format = null, array $context = []): AbstractStructuralDBElement|null
|
||||
{
|
||||
//Retrieve the repository for the given type
|
||||
/** @var StructuralDBElementRepository<T> $repo */
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class StructuralElementNormalizer implements NormalizerInterface
|
|||
{
|
||||
}
|
||||
|
||||
public function supportsNormalization($data, string $format = null, array $context = []): bool
|
||||
public function supportsNormalization($data, ?string $format = null, array $context = []): bool
|
||||
{
|
||||
//Only normalize if we are doing a file export operation
|
||||
if (!($context['partdb_export'] ?? false)) {
|
||||
|
|
@ -48,7 +48,7 @@ class StructuralElementNormalizer implements NormalizerInterface
|
|||
return $data instanceof AbstractStructuralDBElement;
|
||||
}
|
||||
|
||||
public function normalize($object, string $format = null, array $context = []): mixed
|
||||
public function normalize($object, ?string $format = null, array $context = []): mixed
|
||||
{
|
||||
if (!$object instanceof AbstractStructuralDBElement) {
|
||||
throw new \InvalidArgumentException('This normalizer only supports AbstractStructural objects!');
|
||||
|
|
|
|||
|
|
@ -357,7 +357,7 @@ class EntityImporter
|
|||
* @param iterable $entities the list of entities that should be fixed
|
||||
* @param AbstractStructuralDBElement|null $parent the parent, to which the entity should be set
|
||||
*/
|
||||
protected function correctParentEntites(iterable $entities, AbstractStructuralDBElement $parent = null): void
|
||||
protected function correctParentEntites(iterable $entities, ?AbstractStructuralDBElement $parent = null): void
|
||||
{
|
||||
foreach ($entities as $entity) {
|
||||
/** @var AbstractStructuralDBElement $entity */
|
||||
|
|
|
|||
|
|
@ -72,9 +72,9 @@ class ParameterDTO
|
|||
group: $group);
|
||||
}
|
||||
|
||||
//If the attribute contains "..." or a tilde we assume it is a range
|
||||
if (preg_match('/(\.{3}|~)/', $value) === 1) {
|
||||
$parts = preg_split('/\s*(\.{3}|~)\s*/', $value);
|
||||
//If the attribute contains ".." or "..." or a tilde we assume it is a range
|
||||
if (preg_match('/(\.{2,3}|~)/', $value) === 1) {
|
||||
$parts = preg_split('/\s*(\.{2,3}|~)\s*/', $value);
|
||||
if (count($parts) === 2) {
|
||||
//Try to extract number and unit from value (allow leading +)
|
||||
if ($unit === null || trim($unit) === '') {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ use App\Entity\Parts\Part;
|
|||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
|
|
@ -34,10 +35,12 @@ final class PartInfoRetriever
|
|||
{
|
||||
|
||||
private const CACHE_DETAIL_EXPIRATION = 60 * 60 * 24 * 4; // 4 days
|
||||
private const CACHE_RESULT_EXPIRATION = 60 * 60 * 24 * 7; // 7 days
|
||||
private const CACHE_RESULT_EXPIRATION = 60 * 60 * 24 * 4; // 7 days
|
||||
|
||||
public function __construct(private readonly ProviderRegistry $provider_registry,
|
||||
private readonly DTOtoEntityConverter $dto_to_entity_converter, private readonly CacheInterface $partInfoCache)
|
||||
private readonly DTOtoEntityConverter $dto_to_entity_converter, private readonly CacheInterface $partInfoCache,
|
||||
#[Autowire(param: "kernel.debug")]
|
||||
private readonly bool $debugMode = false)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +59,11 @@ final class PartInfoRetriever
|
|||
$provider = $this->provider_registry->getProviderByKey($provider);
|
||||
}
|
||||
|
||||
//Ensure that the provider is active
|
||||
if (!$provider->isActive()) {
|
||||
throw new \RuntimeException("The provider with key {$provider->getProviderKey()} is not active!");
|
||||
}
|
||||
|
||||
if (!$provider instanceof InfoProviderInterface) {
|
||||
throw new \InvalidArgumentException("The provider must be either a provider key or a provider instance!");
|
||||
}
|
||||
|
|
@ -77,7 +85,7 @@ final class PartInfoRetriever
|
|||
$escaped_keyword = urlencode($keyword);
|
||||
return $this->partInfoCache->get("search_{$provider->getProviderKey()}_{$escaped_keyword}", function (ItemInterface $item) use ($provider, $keyword) {
|
||||
//Set the expiration time
|
||||
$item->expiresAfter(self::CACHE_RESULT_EXPIRATION);
|
||||
$item->expiresAfter(!$this->debugMode ? self::CACHE_RESULT_EXPIRATION : 1);
|
||||
|
||||
return $provider->searchByKeyword($keyword);
|
||||
});
|
||||
|
|
@ -94,11 +102,16 @@ final class PartInfoRetriever
|
|||
{
|
||||
$provider = $this->provider_registry->getProviderByKey($provider_key);
|
||||
|
||||
//Ensure that the provider is active
|
||||
if (!$provider->isActive()) {
|
||||
throw new \RuntimeException("The provider with key $provider_key is not active!");
|
||||
}
|
||||
|
||||
//Generate key and escape reserved characters from the provider id
|
||||
$escaped_part_id = urlencode($part_id);
|
||||
return $this->partInfoCache->get("details_{$provider_key}_{$escaped_part_id}", function (ItemInterface $item) use ($provider, $part_id) {
|
||||
//Set the expiration time
|
||||
$item->expiresAfter(self::CACHE_DETAIL_EXPIRATION);
|
||||
$item->expiresAfter(!$this->debugMode ? self::CACHE_DETAIL_EXPIRATION : 1);
|
||||
|
||||
return $provider->getDetails($part_id);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class LCSCProvider implements InfoProviderInterface
|
|||
'Cookie' => new Cookie('currencyCode', $this->currency)
|
||||
],
|
||||
'query' => [
|
||||
'productCode' => $id,
|
||||
'prductCode' => $id,
|
||||
],
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1221,7 +1221,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
* - 'value_min' => string|null The minimum value in a range, if applicable.
|
||||
* - 'value_max' => string|null The maximum value in a range, if applicable.
|
||||
*/
|
||||
private function customSplitIntoValueAndUnit(string $value1, string $value2 = null): array
|
||||
private function customSplitIntoValueAndUnit(string $value1, ?string $value2 = null): array
|
||||
{
|
||||
// Separate numbers and units (basic parsing handling)
|
||||
$unit = null;
|
||||
|
|
|
|||
249
src/Services/InfoProviderSystem/Providers/PollinProvider.php
Normal file
249
src/Services/InfoProviderSystem/Providers/PollinProvider.php
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
<?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\InfoProviderSystem\Providers;
|
||||
|
||||
use App\Entity\Parts\ManufacturingStatus;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Services\InfoProviderSystem\DTOs\FileDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class PollinProvider implements InfoProviderInterface
|
||||
{
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client,
|
||||
#[Autowire(env: 'bool:PROVIDER_POLLIN_ENABLED')]
|
||||
private readonly bool $enabled = true,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getProviderInfo(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Pollin',
|
||||
'description' => 'Webscrapping from pollin.de to get part information',
|
||||
'url' => 'https://www.reichelt.de/',
|
||||
'disabled_help' => 'Set PROVIDER_POLLIN_ENABLED env to 1'
|
||||
];
|
||||
}
|
||||
|
||||
public function getProviderKey(): string
|
||||
{
|
||||
return 'pollin';
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
{
|
||||
$response = $this->client->request('GET', 'https://www.pollin.de/search', [
|
||||
'query' => [
|
||||
'search' => $keyword
|
||||
]
|
||||
]);
|
||||
|
||||
$content = $response->getContent();
|
||||
|
||||
//If the response has us redirected to the product page, then just return the single item
|
||||
if ($response->getInfo('redirect_count') > 0) {
|
||||
return [$this->parseProductPage($content)];
|
||||
}
|
||||
|
||||
$dom = new Crawler($content);
|
||||
|
||||
$results = [];
|
||||
|
||||
//Iterate over each div.product-box
|
||||
$dom->filter('div.product-box')->each(function (Crawler $node) use (&$results) {
|
||||
$results[] = new SearchResultDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $node->filter('meta[itemprop="productID"]')->attr('content'),
|
||||
name: $node->filter('a.product-name')->text(),
|
||||
description: '',
|
||||
preview_image_url: $node->filter('img.product-image')->attr('src'),
|
||||
manufacturing_status: $this->mapAvailability($node->filter('link[itemprop="availability"]')->attr('href')),
|
||||
provider_url: $node->filter('a.product-name')->attr('href')
|
||||
);
|
||||
});
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function mapAvailability(string $availabilityURI): ManufacturingStatus
|
||||
{
|
||||
return match( $availabilityURI) {
|
||||
'http://schema.org/InStock' => ManufacturingStatus::ACTIVE,
|
||||
'http://schema.org/OutOfStock' => ManufacturingStatus::DISCONTINUED,
|
||||
default => ManufacturingStatus::NOT_SET
|
||||
};
|
||||
}
|
||||
|
||||
public function getDetails(string $id): PartDetailDTO
|
||||
{
|
||||
//Ensure that $id is numeric
|
||||
if (!is_numeric($id)) {
|
||||
throw new \InvalidArgumentException("The id must be numeric!");
|
||||
}
|
||||
|
||||
$response = $this->client->request('GET', 'https://www.pollin.de/search', [
|
||||
'query' => [
|
||||
'search' => $id
|
||||
]
|
||||
]);
|
||||
|
||||
//The response must have us redirected to the product page
|
||||
if ($response->getInfo('redirect_count') > 0) {
|
||||
throw new \RuntimeException("Could not resolve the product page for the given id!");
|
||||
}
|
||||
|
||||
$content = $response->getContent();
|
||||
|
||||
return $this->parseProductPage($content);
|
||||
}
|
||||
|
||||
private function parseProductPage(string $content): PartDetailDTO
|
||||
{
|
||||
$dom = new Crawler($content);
|
||||
|
||||
$productPageUrl = $dom->filter('meta[property="product:product_link"]')->attr('content');
|
||||
$orderId = trim($dom->filter('span[itemprop="sku"]')->text()); //Text is important here
|
||||
|
||||
//Calculate the mass
|
||||
$massStr = $dom->filter('meta[itemprop="weight"]')->attr('content');
|
||||
//Remove the unit
|
||||
$massStr = str_replace('kg', '', $massStr);
|
||||
//Convert to float and convert to grams
|
||||
$mass = (float) $massStr * 1000;
|
||||
|
||||
//Parse purchase info
|
||||
$purchaseInfo = new PurchaseInfoDTO('Pollin', $orderId, $this->parsePrices($dom), $productPageUrl);
|
||||
|
||||
return new PartDetailDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $orderId,
|
||||
name: trim($dom->filter('meta[property="og:title"]')->attr('content')),
|
||||
description: $dom->filter('meta[property="og:description"]')->attr('content'),
|
||||
category: $this->parseCategory($dom),
|
||||
manufacturer: $dom->filter('meta[property="product:brand"]')->count() > 0 ? $dom->filter('meta[property="product:brand"]')->attr('content') : null,
|
||||
preview_image_url: $dom->filter('meta[property="og:image"]')->attr('content'),
|
||||
manufacturing_status: $this->mapAvailability($dom->filter('link[itemprop="availability"]')->attr('href')),
|
||||
provider_url: $productPageUrl,
|
||||
notes: $this->parseNotes($dom),
|
||||
datasheets: $this->parseDatasheets($dom),
|
||||
parameters: $this->parseParameters($dom),
|
||||
vendor_infos: [$purchaseInfo],
|
||||
mass: $mass,
|
||||
);
|
||||
}
|
||||
|
||||
private function parseDatasheets(Crawler $dom): array
|
||||
{
|
||||
//Iterate over each a element withing div.pol-product-detail-download-files
|
||||
$datasheets = [];
|
||||
$dom->filter('div.pol-product-detail-download-files a')->each(function (Crawler $node) use (&$datasheets) {
|
||||
$datasheets[] = new FileDTO($node->attr('href'), $node->text());
|
||||
});
|
||||
|
||||
return $datasheets;
|
||||
}
|
||||
|
||||
private function parseParameters(Crawler $dom): array
|
||||
{
|
||||
$parameters = [];
|
||||
|
||||
//Iterate over each tr.properties-row inside table.product-detail-properties-table
|
||||
$dom->filter('table.product-detail-properties-table tr.properties-row')->each(function (Crawler $node) use (&$parameters) {
|
||||
$parameters[] = ParameterDTO::parseValueIncludingUnit(
|
||||
name: rtrim($node->filter('th.properties-label')->text(), ':'),
|
||||
value: trim($node->filter('td.properties-value')->text())
|
||||
);
|
||||
});
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
private function parseCategory(Crawler $dom): string
|
||||
{
|
||||
$category = '';
|
||||
|
||||
//Iterate over each li.breadcrumb-item inside ol.breadcrumb
|
||||
$dom->filter('ol.breadcrumb li.breadcrumb-item')->each(function (Crawler $node) use (&$category) {
|
||||
//Skip if it has breadcrumb-item-home class
|
||||
if (str_contains($node->attr('class'), 'breadcrumb-item-home')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$category .= $node->text() . ' -> ';
|
||||
});
|
||||
|
||||
//Remove the last ' -> '
|
||||
return substr($category, 0, -4);
|
||||
}
|
||||
|
||||
private function parseNotes(Crawler $dom): string
|
||||
{
|
||||
//Concat product highlights and product description
|
||||
return $dom->filter('div.product-detail-top-features')->html() . '<br><br>' . $dom->filter('div.product-detail-description-text')->html();
|
||||
}
|
||||
|
||||
private function parsePrices(Crawler $dom): array
|
||||
{
|
||||
//TODO: Properly handle multiple prices, for now we just look at the price for one piece
|
||||
|
||||
//We assume the currency is always the same
|
||||
$currency = $dom->filter('meta[property="product:price:currency"]')->attr('content');
|
||||
|
||||
//If there is meta[property=highPrice] then use this as the price
|
||||
if ($dom->filter('meta[itemprop="highPrice"]')->count() > 0) {
|
||||
$price = $dom->filter('meta[itemprop="highPrice"]')->attr('content');
|
||||
} else {
|
||||
$price = $dom->filter('meta[property="product:price:amount"]')->attr('content');
|
||||
}
|
||||
|
||||
return [
|
||||
new PriceDTO(1.0, $price, $currency)
|
||||
];
|
||||
}
|
||||
|
||||
public function getCapabilities(): array
|
||||
{
|
||||
return [
|
||||
ProviderCapabilities::BASIC,
|
||||
ProviderCapabilities::PICTURE,
|
||||
ProviderCapabilities::PRICE,
|
||||
ProviderCapabilities::DATASHEET
|
||||
];
|
||||
}
|
||||
}
|
||||
285
src/Services/InfoProviderSystem/Providers/ReicheltProvider.php
Normal file
285
src/Services/InfoProviderSystem/Providers/ReicheltProvider.php
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
<?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\InfoProviderSystem\Providers;
|
||||
|
||||
use App\Services\InfoProviderSystem\DTOs\FileDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class ReicheltProvider implements InfoProviderInterface
|
||||
{
|
||||
|
||||
public const DISTRIBUTOR_NAME = "Reichelt";
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client,
|
||||
#[Autowire(env: "bool:PROVIDER_REICHELT_ENABLED")]
|
||||
private readonly bool $enabled = true,
|
||||
#[Autowire(env: "PROVIDER_REICHELT_LANGUAGE")]
|
||||
private readonly string $language = "en",
|
||||
#[Autowire(env: "PROVIDER_REICHELT_COUNTRY")]
|
||||
private readonly string $country = "DE",
|
||||
#[Autowire(env: "PROVIDER_REICHELT_INCLUDE_VAT")]
|
||||
private readonly bool $includeVAT = false,
|
||||
#[Autowire(env: "PROVIDER_REICHELT_CURRENCY")]
|
||||
private readonly string $currency = "EUR",
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getProviderInfo(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Reichelt',
|
||||
'description' => 'Webscrapping from reichelt.com to get part information',
|
||||
'url' => 'https://www.reichelt.com/',
|
||||
'disabled_help' => 'Set PROVIDER_REICHELT_ENABLED env to 1'
|
||||
];
|
||||
}
|
||||
|
||||
public function getProviderKey(): string
|
||||
{
|
||||
return 'reichelt';
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
{
|
||||
$response = $this->client->request('GET', sprintf($this->getBaseURL() . '/shop/search/%s', $keyword));
|
||||
$html = $response->getContent();
|
||||
|
||||
//Parse the HTML and return the results
|
||||
$dom = new Crawler($html);
|
||||
//Iterate over all div.al_gallery_article elements
|
||||
$results = [];
|
||||
$dom->filter('div.al_gallery_article')->each(function (Crawler $element) use (&$results) {
|
||||
|
||||
//Extract product id from data-product attribute
|
||||
$artId = json_decode($element->attr('data-product'), true, 2, JSON_THROW_ON_ERROR)['artid'];
|
||||
|
||||
$productID = $element->filter('meta[itemprop="productID"]')->attr('content');
|
||||
$name = $element->filter('meta[itemprop="name"]')->attr('content');
|
||||
$sku = $element->filter('meta[itemprop="sku"]')->attr('content');
|
||||
|
||||
//Try to extract a picture URL:
|
||||
$pictureURL = $element->filter("div.al_artlogo img")->attr('src');
|
||||
|
||||
$results[] = new SearchResultDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $artId,
|
||||
name: $productID,
|
||||
description: $name,
|
||||
category: null,
|
||||
manufacturer: $sku,
|
||||
preview_image_url: $pictureURL,
|
||||
provider_url: $element->filter('a.al_artinfo_link')->attr('href')
|
||||
);
|
||||
});
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getDetails(string $id): PartDetailDTO
|
||||
{
|
||||
//Check that the ID is a number
|
||||
if (!is_numeric($id)) {
|
||||
throw new \InvalidArgumentException("Invalid ID");
|
||||
}
|
||||
|
||||
//Use this endpoint to resolve the artID to a product page
|
||||
$response = $this->client->request('GET',
|
||||
sprintf(
|
||||
'https://www.reichelt.com/?ACTION=514&id=74&article=%s&LANGUAGE=%s&CCOUNTRY=%s',
|
||||
$id,
|
||||
strtoupper($this->language),
|
||||
strtoupper($this->country)
|
||||
)
|
||||
);
|
||||
$json = $response->toArray();
|
||||
|
||||
//Retrieve the product page from the response
|
||||
$productPage = $this->getBaseURL() . '/shop/product' . $json[0]['article_path'];
|
||||
|
||||
|
||||
$response = $this->client->request('GET', $productPage, [
|
||||
'query' => [
|
||||
'CCTYPE' => $this->includeVAT ? 'private' : 'business',
|
||||
'currency' => $this->currency,
|
||||
],
|
||||
]);
|
||||
$html = $response->getContent();
|
||||
$dom = new Crawler($html);
|
||||
|
||||
//Extract the product notes
|
||||
$notes = $dom->filter('p[itemprop="description"]')->html();
|
||||
|
||||
//Extract datasheets
|
||||
$datasheets = [];
|
||||
$dom->filter('div.articleDatasheet a')->each(function (Crawler $element) use (&$datasheets) {
|
||||
$datasheets[] = new FileDTO($element->attr('href'), $element->filter('span')->text());
|
||||
});
|
||||
|
||||
//Determine price for one unit
|
||||
$priceString = $dom->filter('meta[itemprop="price"]')->attr('content');
|
||||
$currency = $dom->filter('meta[itemprop="priceCurrency"]')->attr('content', 'EUR');
|
||||
|
||||
//Create purchase info
|
||||
$purchaseInfo = new PurchaseInfoDTO(
|
||||
distributor_name: self::DISTRIBUTOR_NAME,
|
||||
order_number: $json[0]['article_artnr'],
|
||||
prices: array_merge(
|
||||
[new PriceDTO(1.0, $priceString, $currency, $this->includeVAT)]
|
||||
, $this->parseBatchPrices($dom, $currency)),
|
||||
product_url: $productPage
|
||||
);
|
||||
|
||||
//Create part object
|
||||
return new PartDetailDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $id,
|
||||
name: $json[0]['article_artnr'],
|
||||
description: $json[0]['article_besch'],
|
||||
category: $this->parseCategory($dom),
|
||||
manufacturer: $json[0]['manufacturer_name'],
|
||||
mpn: $this->parseMPN($dom),
|
||||
preview_image_url: $json[0]['article_picture'],
|
||||
provider_url: $productPage,
|
||||
notes: $notes,
|
||||
datasheets: $datasheets,
|
||||
parameters: $this->parseParameters($dom),
|
||||
vendor_infos: [$purchaseInfo]
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private function parseMPN(Crawler $dom): string
|
||||
{
|
||||
//Find the small element directly after meta[itemprop="url"] element
|
||||
$element = $dom->filter('meta[itemprop="url"] + small');
|
||||
//If the text contains GTIN text, take the small element afterwards
|
||||
if (str_contains($element->text(), 'GTIN')) {
|
||||
$element = $dom->filter('meta[itemprop="url"] + small + small');
|
||||
}
|
||||
|
||||
//The MPN is contained in the span inside the element
|
||||
return $element->filter('span')->text();
|
||||
}
|
||||
|
||||
private function parseBatchPrices(Crawler $dom, string $currency): array
|
||||
{
|
||||
//Iterate over each a.inline-block element in div.discountValue
|
||||
$prices = [];
|
||||
$dom->filter('div.discountValue a.inline-block')->each(function (Crawler $element) use (&$prices, $currency) {
|
||||
//The minimum amount is the number in the span.block element
|
||||
$minAmountText = $element->filter('span.block')->text();
|
||||
|
||||
//Extract a integer from the text
|
||||
$matches = [];
|
||||
if (!preg_match('/\d+/', $minAmountText, $matches)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$minAmount = (int) $matches[0];
|
||||
|
||||
//The price is the text of the p.productPrice element
|
||||
$priceString = $element->filter('p.productPrice')->text();
|
||||
//Replace comma with dot
|
||||
$priceString = str_replace(',', '.', $priceString);
|
||||
//Strip any non-numeric characters
|
||||
$priceString = preg_replace('/[^0-9.]/', '', $priceString);
|
||||
|
||||
$prices[] = new PriceDTO($minAmount, $priceString, $currency, $this->includeVAT);
|
||||
});
|
||||
|
||||
return $prices;
|
||||
}
|
||||
|
||||
|
||||
private function parseCategory(Crawler $dom): string
|
||||
{
|
||||
// Look for ol.breadcrumb and iterate over the li elements
|
||||
$category = '';
|
||||
$dom->filter('ol.breadcrumb li.triangle-left')->each(function (Crawler $element) use (&$category) {
|
||||
//Do not include the .breadcrumb-showmore element
|
||||
if ($element->attr('id') === 'breadcrumb-showmore') {
|
||||
return;
|
||||
}
|
||||
|
||||
$category .= $element->text() . ' -> ';
|
||||
});
|
||||
//Remove the trailing ' -> '
|
||||
$category = substr($category, 0, -4);
|
||||
|
||||
return $category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Crawler $dom
|
||||
* @return ParameterDTO[]
|
||||
*/
|
||||
private function parseParameters(Crawler $dom): array
|
||||
{
|
||||
$parameters = [];
|
||||
//Iterate over each ul.articleTechnicalData which contains the specifications of each group
|
||||
$dom->filter('ul.articleTechnicalData')->each(function (Crawler $groupElement) use (&$parameters) {
|
||||
$groupName = $groupElement->filter('li.articleTechnicalHeadline')->text();
|
||||
|
||||
//Iterate over each second li in ul.articleAttribute, which contains the specifications
|
||||
$groupElement->filter('ul.articleAttribute li:nth-child(2n)')->each(function (Crawler $specElement) use (&$parameters, $groupName) {
|
||||
$parameters[] = ParameterDTO::parseValueIncludingUnit(
|
||||
name: $specElement->previousAll()->text(),
|
||||
value: $specElement->text(),
|
||||
group: $groupName
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
private function getBaseURL(): string
|
||||
{
|
||||
//Without the trailing slash
|
||||
return 'https://www.reichelt.com/' . strtolower($this->country) . '/' . strtolower($this->language);
|
||||
}
|
||||
|
||||
public function getCapabilities(): array
|
||||
{
|
||||
return [
|
||||
ProviderCapabilities::BASIC,
|
||||
ProviderCapabilities::PICTURE,
|
||||
ProviderCapabilities::DATASHEET,
|
||||
ProviderCapabilities::PRICE,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -43,12 +43,12 @@ class UniqueObjectCollection extends Constraint
|
|||
* @param array|string $fields the combination of fields that must contain unique values or a set of options
|
||||
*/
|
||||
public function __construct(
|
||||
array $options = null,
|
||||
string $message = null,
|
||||
callable $normalizer = null,
|
||||
array $groups = null,
|
||||
?array $options = null,
|
||||
?string $message = null,
|
||||
?callable $normalizer = null,
|
||||
?array $groups = null,
|
||||
mixed $payload = null,
|
||||
array|string $fields = null,
|
||||
array|string|null $fields = null,
|
||||
public bool $allowNull = true,
|
||||
) {
|
||||
parent::__construct($options, $groups, $payload);
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ class ValidGoogleAuthCode extends Constraint
|
|||
* @param TwoFactorInterface|null $user The user to use for the validation process, if null, the current user is used
|
||||
*/
|
||||
public function __construct(
|
||||
array $options = null,
|
||||
array $groups = null,
|
||||
?array $options = null,
|
||||
?array $groups = null,
|
||||
mixed $payload = null,
|
||||
public ?TwoFactorInterface $user = null)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue