Merge branch 'master' into settings-bundle

This commit is contained in:
Jan Böhmer 2024-06-24 21:15:14 +02:00
commit 3e657a7cac
305 changed files with 7543 additions and 4274 deletions

View file

@ -81,12 +81,12 @@ class EntityFilterHelper
public function getDescription(array $properties): array
{
if (!$properties) {
if ($properties === []) {
return [];
}
$description = [];
foreach ($properties as $property => $strategy) {
foreach (array_keys($properties) as $property) {
$description[(string)$property] = [
'property' => $property,
'type' => Type::BUILTIN_TYPE_STRING,

View file

@ -61,7 +61,7 @@ final class LikeFilter extends AbstractFilter
}
$description = [];
foreach ($this->properties as $property => $strategy) {
foreach (array_keys($this->properties) as $property) {
$description[(string)$property] = [
'property' => $property,
'type' => Type::BUILTIN_TYPE_STRING,

View file

@ -46,7 +46,7 @@ final class HandleAttachmentsUploadsProcessor implements ProcessorInterface
}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
if ($operation instanceof DeleteOperationInterface) {
return $this->removeProcessor->process($data, $operation, $uriVariables, $context);

View file

@ -4,8 +4,10 @@ declare(strict_types=1);
namespace App\Command;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Spatie\DbDumper\Databases\PostgreSql;
use Symfony\Component\Console\Attribute\AsCommand;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\ORM\EntityManagerInterface;
use PhpZip\Constants\ZipCompressionMethod;
@ -50,6 +52,7 @@ class BackupCommand extends Command
$backup_attachments = $input->getOption('attachments');
$backup_config = $input->getOption('config');
$backup_full = $input->getOption('full');
$overwrite = $input->getOption('overwrite');
if ($backup_full) {
$backup_database = true;
@ -68,7 +71,9 @@ class BackupCommand extends Command
//Check if the file already exists
//Then ask the user, if he wants to overwrite the file
if (file_exists($output_filepath) && !$io->confirm('The file '.realpath($output_filepath).' already exists. Do you want to overwrite it?', false)) {
if (!$overwrite
&& file_exists($output_filepath)
&& !$io->confirm('The file '.realpath($output_filepath).' already exists. Do you want to overwrite it?', false)) {
$io->error('Backup aborted!');
return Command::FAILURE;
}
@ -136,30 +141,42 @@ class BackupCommand extends Command
}
}
private function runSQLDumper(DbDumper $dumper, ZipFile $zip, array $connectionParams): void
{
$this->configureDumper($connectionParams, $dumper);
$tmp_file = tempnam(sys_get_temp_dir(), 'partdb_sql_dump');
$dumper->dumpToFile($tmp_file);
$zip->addFile($tmp_file, 'database.sql');
}
protected function backupDatabase(ZipFile $zip, SymfonyStyle $io): void
{
$io->note('Backup database...');
//Determine if we use MySQL or SQLite
$connection = $this->entityManager->getConnection();
if ($connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) {
$params = $connection->getParams();
$platform = $connection->getDatabasePlatform();
if ($platform instanceof AbstractMySQLPlatform) {
try {
$io->note('MySQL database detected. Dump DB to SQL using mysqldump...');
$params = $connection->getParams();
$dumper = MySql::create();
$this->configureDumper($params, $dumper);
$tmp_file = tempnam(sys_get_temp_dir(), 'partdb_sql_dump');
$dumper->dumpToFile($tmp_file);
$zip->addFile($tmp_file, 'mysql_dump.sql');
$this->runSQLDumper(MySql::create(), $zip, $params);
} catch (\Exception $e) {
$io->error('Could not dump database: '.$e->getMessage());
$io->error('This can maybe be fixed by installing the mysqldump binary and adding it to the PATH variable!');
}
} elseif ($connection->getDatabasePlatform() instanceof SqlitePlatform) {
} elseif ($platform instanceof PostgreSQLPlatform) {
try {
$io->note('PostgreSQL database detected. Dump DB to SQL using pg_dump...');
$this->runSQLDumper(PostgreSql::create(), $zip, $params);
} catch (\Exception $e) {
$io->error('Could not dump database: '.$e->getMessage());
$io->error('This can maybe be fixed by installing the pg_dump binary and adding it to the PATH variable!');
}
} elseif ($platform instanceof SQLitePlatform) {
$io->note('SQLite database detected. Copy DB file to ZIP...');
$params = $connection->getParams();
$zip->addFile($params['path'], 'var/app.db');
} else {
$io->error('Unknown database platform. Could not backup database!');

View file

@ -52,7 +52,6 @@ use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use Omines\DataTablesBundle\DataTableFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@ -61,6 +60,8 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function Symfony\Component\Translation\t;
@ -74,15 +75,10 @@ abstract class BaseAdminController extends AbstractController
protected string $attachment_class = '';
protected ?string $parameter_class = '';
/**
* @var EventDispatcher|EventDispatcherInterface
*/
protected EventDispatcher|EventDispatcherInterface $eventDispatcher;
public function __construct(protected TranslatorInterface $translator, protected UserPasswordHasherInterface $passwordEncoder,
protected AttachmentSubmitHandler $attachmentSubmitHandler,
protected EventCommentHelper $commentHelper, protected HistoryHelper $historyHelper, protected TimeTravel $timeTravel,
protected DataTableFactory $dataTableFactory, EventDispatcherInterface $eventDispatcher, protected LabelExampleElementsGenerator $barcodeExampleGenerator,
protected DataTableFactory $dataTableFactory, protected EventDispatcherInterface $eventDispatcher, protected LabelExampleElementsGenerator $barcodeExampleGenerator,
protected LabelGenerator $labelGenerator, protected EntityManagerInterface $entityManager)
{
if ('' === $this->entity_class || '' === $this->form_class || '' === $this->twig_template || '' === $this->route_base) {
@ -96,7 +92,6 @@ abstract class BaseAdminController extends AbstractController
if ('' === $this->parameter_class || ($this->parameter_class && !is_a($this->parameter_class, AbstractParameter::class, true))) {
throw new InvalidArgumentException('You have to override the $parameter_class value with a valid Parameter class in your subclass!');
}
$this->eventDispatcher = $eventDispatcher;
}
protected function revertElementIfNeeded(AbstractDBElement $entity, ?string $timestamp): ?DateTime
@ -192,10 +187,8 @@ abstract class BaseAdminController extends AbstractController
}
//Ensure that the master picture is still part of the attachments
if ($entity instanceof AttachmentContainingDBElement) {
if ($entity->getMasterPictureAttachment() !== null && !$entity->getAttachments()->contains($entity->getMasterPictureAttachment())) {
$entity->setMasterPictureAttachment(null);
}
if ($entity instanceof AttachmentContainingDBElement && ($entity->getMasterPictureAttachment() !== null && !$entity->getAttachments()->contains($entity->getMasterPictureAttachment()))) {
$entity->setMasterPictureAttachment(null);
}
$this->commentHelper->setMessage($form['log_comment']->getData());
@ -283,10 +276,8 @@ abstract class BaseAdminController extends AbstractController
}
//Ensure that the master picture is still part of the attachments
if ($new_entity instanceof AttachmentContainingDBElement) {
if ($new_entity->getMasterPictureAttachment() !== null && !$new_entity->getAttachments()->contains($new_entity->getMasterPictureAttachment())) {
$new_entity->setMasterPictureAttachment(null);
}
if ($new_entity instanceof AttachmentContainingDBElement && ($new_entity->getMasterPictureAttachment() !== null && !$new_entity->getAttachments()->contains($new_entity->getMasterPictureAttachment()))) {
$new_entity->setMasterPictureAttachment(null);
}
$this->commentHelper->setMessage($form['log_comment']->getData());
@ -333,8 +324,8 @@ abstract class BaseAdminController extends AbstractController
try {
$errors = $importer->importFileAndPersistToDB($file, $options);
foreach ($errors as $name => $error) {
foreach ($error as $violation) {
foreach ($errors as $name => ['violations' => $violations]) {
foreach ($violations as $violation) {
$this->addFlash('error', $name.': '.$violation->getMessage());
}
}
@ -344,6 +335,7 @@ abstract class BaseAdminController extends AbstractController
}
}
ret:
//Mass creation form
$mass_creation_form = $this->createForm(MassCreationForm::class, ['entity_class' => $this->entity_class]);
$mass_creation_form->handleRequest($request);
@ -356,11 +348,14 @@ abstract class BaseAdminController extends AbstractController
$results = $importer->massCreation($data['lines'], $this->entity_class, $data['parent'] ?? null, $errors);
//Show errors to user:
foreach ($errors as $error) {
if ($error['entity'] instanceof AbstractStructuralDBElement) {
$this->addFlash('error', $error['entity']->getFullPath().':'.$error['violations']);
} else { //When we don't have a structural element, we can only show the name
$this->addFlash('error', $error['entity']->getName().':'.$error['violations']);
foreach ($errors as ['entity' => $new_entity, 'violations' => $violations]) {
/** @var ConstraintViolationInterface $violation */
foreach ($violations as $violation) {
if ($new_entity instanceof AbstractStructuralDBElement) {
$this->addFlash('error', $new_entity->getFullPath().':'.$violation->getMessage());
} else { //When we don't have a structural element, we can only show the name
$this->addFlash('error', $new_entity->getName().':'.$violation->getMessage());
}
}
}
@ -371,11 +366,10 @@ abstract class BaseAdminController extends AbstractController
$em->flush();
if (count($results) > 0) {
$this->addFlash('success', t('entity.mass_creation_flash', ['%COUNT%' => count($results)]));
$this->addFlash('success', t('entity.mass_creation_flash', ['%COUNT%' => count($results)]));
}
}
ret:
return $this->render($this->twig_template, [
'entity' => $new_entity,
'form' => $form,

View file

@ -30,6 +30,9 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
/**
* @see \App\Tests\Controller\KiCadApiControllerTest
*/
#[Route('/kicad-api/v1')]
class KiCadApiController extends AbstractController
{
@ -62,7 +65,7 @@ class KiCadApiController extends AbstractController
#[Route('/parts/category/{category}.json', name: 'kicad_api_category')]
public function categoryParts(?Category $category): Response
{
if ($category) {
if ($category !== null) {
$this->denyAccessUnlessGranted('read', $category);
} else {
$this->denyAccessUnlessGranted('@categories.read');

View file

@ -151,7 +151,7 @@ class LogController extends AbstractController
if (EventUndoMode::UNDO === $mode) {
$this->undoLog($log_element);
} elseif (EventUndoMode::REVERT === $mode) {
} else {
$this->revertLog($log_element);
}

View file

@ -51,7 +51,7 @@ class OAuthClientController extends AbstractController
}
#[Route('/{name}/check', name: 'oauth_client_check')]
public function check(string $name, Request $request): Response
public function check(string $name): Response
{
$this->denyAccessUnlessGranted('@system.manage_oauth_tokens');

View file

@ -329,7 +329,7 @@ class PartController extends AbstractController
$this->em->flush();
if ($mode === 'new') {
$this->addFlash('success', 'part.created_flash');
} else if ($mode === 'edit') {
} elseif ($mode === 'edit') {
$this->addFlash('success', 'part.edited_flash');
}
@ -358,11 +358,11 @@ class PartController extends AbstractController
$template = '';
if ($mode === 'new') {
$template = 'parts/edit/new_part.html.twig';
} else if ($mode === 'edit') {
} elseif ($mode === 'edit') {
$template = 'parts/edit/edit_part_info.html.twig';
} else if ($mode === 'merge') {
} elseif ($mode === 'merge') {
$template = 'parts/edit/merge_parts.html.twig';
} else if ($mode === 'update_from_ip') {
} elseif ($mode === 'update_from_ip') {
$template = 'parts/edit/update_from_ip.html.twig';
}

View file

@ -207,6 +207,11 @@ class ProjectController extends AbstractController
//Preset the BOM entries with the selected parts, when the form was not submitted yet
$preset_data = new ArrayCollection();
foreach (explode(',', (string) $request->get('parts', '')) as $part_id) {
//Skip empty part IDs. Postgres seems to be especially sensitive to empty strings, as it does not allow them in integer columns
if ($part_id === '') {
continue;
}
$part = $entityManager->getRepository(Part::class)->find($part_id);
if (null !== $part) {
//If there is already a BOM entry for this part, we use this one (we edit it then)
@ -214,7 +219,7 @@ class ProjectController extends AbstractController
'project' => $project,
'part' => $part
]);
if ($bom_entry) {
if ($bom_entry !== null) {
$preset_data->add($bom_entry);
} else { //Otherwise create an empty one
$entry = new ProjectBOMEntry();

View file

@ -54,6 +54,9 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\Routing\Attribute\Route;
/**
* @see \App\Tests\Controller\ScanControllerTest
*/
#[Route(path: '/scan')]
class ScanController extends AbstractController
{

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
*/
namespace App\Controller;
use Symfony\Component\Runtime\SymfonyRuntime;
use App\Services\Attachments\AttachmentSubmitHandler;
use App\Services\Attachments\AttachmentURLGenerator;
use App\Services\Attachments\BuiltinAttachmentsFinder;
@ -86,7 +87,7 @@ class ToolsController extends AbstractController
'php_post_max_size' => ini_get('post_max_size'),
'kernel_runtime_environment' => $this->getParameter('kernel.runtime_environment'),
'kernel_runtime_mode' => $this->getParameter('kernel.runtime_mode'),
'kernel_runtime' => $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? 'Symfony\\Component\\Runtime\\SymfonyRuntime',
'kernel_runtime' => $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? SymfonyRuntime::class,
//DB section
'db_type' => $DBInfoHelper->getDatabaseType() ?? 'Unknown',

View file

@ -38,7 +38,6 @@ use Doctrine\ORM\EntityManagerInterface;
use RuntimeException;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
@ -59,11 +58,8 @@ use Symfony\Component\Validator\Constraints\Length;
#[Route(path: '/user')]
class UserSettingsController extends AbstractController
{
protected EventDispatcher|EventDispatcherInterface $eventDispatcher;
public function __construct(protected bool $demo_mode, EventDispatcherInterface $eventDispatcher)
public function __construct(protected bool $demo_mode, protected EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
#[Route(path: '/2fa_backup_codes', name: 'show_backup_codes')]

View file

@ -74,30 +74,37 @@ class DataStructureFixtures extends Fixture implements DependentFixtureInterface
/** @var AbstractStructuralDBElement $node1 */
$node1 = new $class();
$node1->setName('Node 1');
$this->addReference($class . '_1', $node1);
/** @var AbstractStructuralDBElement $node2 */
$node2 = new $class();
$node2->setName('Node 2');
$this->addReference($class . '_2', $node2);
/** @var AbstractStructuralDBElement $node3 */
$node3 = new $class();
$node3->setName('Node 3');
$this->addReference($class . '_3', $node3);
$node1_1 = new $class();
$node1_1->setName('Node 1.1');
$node1_1->setParent($node1);
$this->addReference($class . '_4', $node1_1);
$node1_2 = new $class();
$node1_2->setName('Node 1.2');
$node1_2->setParent($node1);
$this->addReference($class . '_5', $node1_2);
$node2_1 = new $class();
$node2_1->setName('Node 2.1');
$node2_1->setParent($node2);
$this->addReference($class . '_6', $node2_1);
$node1_1_1 = new $class();
$node1_1_1->setName('Node 1.1.1');
$node1_1_1->setParent($node1_1);
$this->addReference($class . '_7', $node1_1_1);
$manager->persist($node1);
$manager->persist($node2);

View file

@ -0,0 +1,106 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 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\DataFixtures;
use App\Entity\LogSystem\ElementCreatedLogEntry;
use App\Entity\LogSystem\ElementDeletedLogEntry;
use App\Entity\LogSystem\ElementEditedLogEntry;
use App\Entity\Parts\Category;
use App\Entity\UserSystem\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
class LogEntryFixtures extends Fixture implements DependentFixtureInterface
{
public function load(ObjectManager $manager)
{
$this->createCategoryEntries($manager);
$this->createDeletedCategory($manager);
}
public function createCategoryEntries(ObjectManager $manager): void
{
$category = $this->getReference(Category::class . '_1', Category::class);
$logEntry = new ElementCreatedLogEntry($category);
$logEntry->setTimestamp(new \DateTimeImmutable("+1 second"));
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
$logEntry->setComment('Test');
$manager->persist($logEntry);
$logEntry = new ElementEditedLogEntry($category);
$logEntry->setTimestamp(new \DateTimeImmutable("+2 second"));
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
$logEntry->setComment('Test');
$logEntry->setOldData(['name' => 'Test']);
$logEntry->setNewData(['name' => 'Node 1.1']);
$manager->persist($logEntry);
$manager->flush();
}
public function createDeletedCategory(ObjectManager $manager): void
{
//We create a fictive category to test the deletion
$category = new Category();
$category->setName('Node 100');
//Assume a category with id 100 was deleted
$reflClass = new \ReflectionClass($category);
$reflClass->getProperty('id')->setValue($category, 100);
//The whole lifecycle from creation to deletion
$logEntry = new ElementCreatedLogEntry($category);
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
$logEntry->setComment('Creation');
$manager->persist($logEntry);
$logEntry = new ElementEditedLogEntry($category);
$logEntry->setTimestamp(new \DateTimeImmutable("+1 second"));
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
$logEntry->setComment('Edit');
$logEntry->setOldData(['name' => 'Test']);
$logEntry->setNewData(['name' => 'Node 100']);
$manager->persist($logEntry);
$logEntry = new ElementDeletedLogEntry($category);
$logEntry->setTimestamp(new \DateTimeImmutable("+2 second"));
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
$logEntry->setOldData(['name' => 'Node 100', 'id' => 100, 'comment' => 'Test comment']);
$manager->persist($logEntry);
$manager->flush();
}
public function getDependencies(): array
{
return [
UserFixtures::class,
DataStructureFixtures::class
];
}
}

View file

@ -73,6 +73,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
$part = new Part();
$part->setName('Part 1');
$part->setCategory($manager->find(Category::class, 1));
$this->addReference(Part::class . '_1', $part);
$manager->persist($part);
/** More complex part */
@ -86,6 +87,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
$part->setIpn('IPN123');
$part->setNeedsReview(true);
$part->setManufacturingStatus(ManufacturingStatus::ACTIVE);
$this->addReference(Part::class . '_2', $part);
$manager->persist($part);
/** Part with orderdetails, storelocations and Attachments */
@ -98,8 +100,9 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
$partLot1->setStorageLocation($manager->find(StorageLocation::class, 1));
$part->addPartLot($partLot1);
$partLot2 = new PartLot();
$partLot2->setExpirationDate(new DateTime());
$partLot2->setExpirationDate(new \DateTimeImmutable());
$partLot2->setComment('Test');
$partLot2->setNeedsRefill(true);
$partLot2->setStorageLocation($manager->find(StorageLocation::class, 3));
@ -133,6 +136,8 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
$attachment->setAttachmentType($manager->find(AttachmentType::class, 1));
$part->addAttachment($attachment);
$this->addReference(Part::class . '_3', $part);
$manager->persist($part);
$manager->flush();
}

View file

@ -50,6 +50,6 @@ class CustomFetchJoinORMAdapter extends FetchJoinORMAdapter
$paginator = new Paginator($qb_without_group_by);
return $paginator->count() ?? 0;
return $paginator->count();
}
}

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\DataTables\Adapters;
use Doctrine\ORM\Query\Expr\From;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Tools\Pagination\Paginator;
@ -51,12 +52,12 @@ class TwoStepORMAdapter extends ORMAdapter
private bool $use_simple_total = false;
private \Closure|null $query_modifier;
private \Closure|null $query_modifier = null;
public function __construct(ManagerRegistry $registry = null)
{
parent::__construct($registry);
$this->detailQueryCallable = static function (QueryBuilder $qb, array $ids) {
$this->detailQueryCallable = static function (QueryBuilder $qb, array $ids): never {
throw new \RuntimeException('You need to set the detail_query option to use the TwoStepORMAdapter');
};
}
@ -66,9 +67,7 @@ class TwoStepORMAdapter extends ORMAdapter
parent::configureOptions($resolver);
$resolver->setRequired('filter_query');
$resolver->setDefault('query', function (Options $options) {
return $options['filter_query'];
});
$resolver->setDefault('query', fn(Options $options) => $options['filter_query']);
$resolver->setRequired('detail_query');
$resolver->setAllowedTypes('detail_query', \Closure::class);
@ -108,7 +107,7 @@ class TwoStepORMAdapter extends ORMAdapter
}
}
/** @var Query\Expr\From $fromClause */
/** @var From $fromClause */
$fromClause = $builder->getDQLPart('from')[0];
$identifier = "{$fromClause->getAlias()}.{$this->metadata->getSingleIdentifierFieldName()}";
@ -129,6 +128,12 @@ class TwoStepORMAdapter extends ORMAdapter
$query->setIdentifierPropertyPath($this->mapFieldToPropertyPath($identifier, $aliases));
}
protected function hasGroupByPart(string $identifier, array $gbList): bool
{
//Always return true, to fix the issue with the count query, when having mutliple group by parts
return true;
}
protected function getCount(QueryBuilder $queryBuilder, $identifier): int
{
if ($this->query_modifier !== null) {
@ -195,7 +200,7 @@ class TwoStepORMAdapter extends ORMAdapter
/** The paginator count queries can be rather slow, so when query for total count (100ms or longer),
* just return the entity count.
*/
/** @var Query\Expr\From $from_expr */
/** @var From $from_expr */
$from_expr = $queryBuilder->getDQLPart('from')[0];
return $this->manager->getRepository($from_expr->getFrom())->count([]);

View file

@ -86,12 +86,13 @@ final class AttachmentDataTable implements DataTableTypeInterface
$dataTable->add('name', TextColumn::class, [
'label' => 'attachment.edit.name',
'orderField' => 'NATSORT(attachment.name)',
'render' => function ($value, Attachment $context) {
//Link to external source
if ($context->isExternal()) {
return sprintf(
'<a href="%s" class="link-external">%s</a>',
htmlspecialchars($context->getURL()),
htmlspecialchars((string) $context->getURL()),
htmlspecialchars($value)
);
}
@ -111,6 +112,7 @@ final class AttachmentDataTable implements DataTableTypeInterface
$dataTable->add('attachment_type', TextColumn::class, [
'label' => 'attachment.table.type',
'field' => 'attachment_type.name',
'orderField' => 'NATSORT(attachment_type.name)',
'render' => fn($value, Attachment $context): string => sprintf(
'<a href="%s">%s</a>',
$this->entityURLGenerator->editURL($context->getAttachmentType()),

View file

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
@ -17,7 +20,6 @@
* 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\DataTables\Column;
use Omines\DataTablesBundle\Column\AbstractColumn;

View file

@ -47,7 +47,7 @@ class LocaleDateTimeColumn extends AbstractColumn
}
if (!$value instanceof DateTimeInterface) {
$value = new DateTime((string) $value);
$value = new \DateTimeImmutable((string) $value);
}
$formatValues = [

View file

@ -22,9 +22,92 @@ declare(strict_types=1);
*/
namespace App\DataTables\Filters\Constraints;
use Doctrine\ORM\QueryBuilder;
use RuntimeException;
/**
* An alias of NumberConstraint to use to filter on a DateTime
* Similar to NumberConstraint but for DateTime values
*/
class DateTimeConstraint extends NumberConstraint
class DateTimeConstraint extends AbstractConstraint
{
protected const ALLOWED_OPERATOR_VALUES = ['=', '!=', '<', '>', '<=', '>=', 'BETWEEN'];
public function __construct(
string $property,
string $identifier = null,
/**
* The value1 used for comparison (this is the main one used for all mono-value comparisons)
*/
protected \DateTimeInterface|null $value1 = null,
protected ?string $operator = null,
/**
* The second value used when operator is RANGE; this is the upper bound of the range
*/
protected \DateTimeInterface|null $value2 = null)
{
parent::__construct($property, $identifier);
}
public function getValue1(): ?\DateTimeInterface
{
return $this->value1;
}
public function setValue1(\DateTimeInterface|null $value1): void
{
$this->value1 = $value1;
}
public function getValue2(): ?\DateTimeInterface
{
return $this->value2;
}
public function setValue2(?\DateTimeInterface $value2): void
{
$this->value2 = $value2;
}
public function getOperator(): string|null
{
return $this->operator;
}
/**
* @param string $operator
*/
public function setOperator(?string $operator): void
{
$this->operator = $operator;
}
public function isEnabled(): bool
{
return $this->value1 !== null
&& ($this->operator !== null && $this->operator !== '');
}
public function apply(QueryBuilder $queryBuilder): void
{
//If no value is provided then we do not apply a filter
if (!$this->isEnabled()) {
return;
}
//Ensure we have an valid operator
if(!in_array($this->operator, self::ALLOWED_OPERATOR_VALUES, true)) {
throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES));
}
if ($this->operator !== 'BETWEEN') {
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value1);
} else {
if ($this->value2 === null) {
throw new RuntimeException("Cannot use operator BETWEEN without value2!");
}
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '1', '>=', $this->value1);
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '2', '<=', $this->value2);
}
}
}

View file

@ -29,12 +29,28 @@ class NumberConstraint extends AbstractConstraint
{
protected const ALLOWED_OPERATOR_VALUES = ['=', '!=', '<', '>', '<=', '>=', 'BETWEEN'];
public function getValue1(): float|int|null|\DateTimeInterface
public function __construct(
string $property,
string $identifier = null,
/**
* The value1 used for comparison (this is the main one used for all mono-value comparisons)
*/
protected float|int|null $value1 = null,
protected ?string $operator = null,
/**
* The second value used when operator is RANGE; this is the upper bound of the range
*/
protected float|int|null $value2 = null)
{
parent::__construct($property, $identifier);
}
public function getValue1(): float|int|null
{
return $this->value1;
}
public function setValue1(float|int|\DateTimeInterface|null $value1): void
public function setValue1(float|int|null $value1): void
{
$this->value1 = $value1;
}
@ -63,22 +79,6 @@ class NumberConstraint extends AbstractConstraint
}
public function __construct(
string $property,
string $identifier = null,
/**
* The value1 used for comparison (this is the main one used for all mono-value comparisons)
*/
protected float|int|\DateTimeInterface|null $value1 = null,
protected ?string $operator = null,
/**
* The second value used when operator is RANGE; this is the upper bound of the range
*/
protected float|int|\DateTimeInterface|null $value2 = null)
{
parent::__construct($property, $identifier);
}
public function isEnabled(): bool
{
return $this->value1 !== null
@ -105,7 +105,13 @@ class NumberConstraint extends AbstractConstraint
}
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '1', '>=', $this->value1);
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '2', '<=', $this->value2);
//Workaround for the amountSum which we need to add twice on postgres. Replace one of the __ with __2 to make it work
//Otherwise we get an error, that __partLot was already defined
$property2 = str_replace('__', '__2', $this->property);
$this->addSimpleAndConstraint($queryBuilder, $property2, $this->identifier . '2', '<=', $this->value2);
}
}
}

View file

@ -23,13 +23,20 @@ declare(strict_types=1);
namespace App\DataTables\Filters\Constraints\Part;
use App\DataTables\Filters\Constraints\BooleanConstraint;
use App\Entity\Parts\PartLot;
use Doctrine\ORM\QueryBuilder;
class LessThanDesiredConstraint extends BooleanConstraint
{
public function __construct(string $property = null, string $identifier = null, ?bool $default_value = null)
{
parent::__construct($property ?? 'amountSum', $identifier, $default_value);
parent::__construct($property ?? '(
SELECT COALESCE(SUM(ld_partLot.amount), 0.0)
FROM '.PartLot::class.' ld_partLot
WHERE ld_partLot.part = part.id
AND ld_partLot.instock_unknown = false
AND (ld_partLot.expiration_date IS NULL OR ld_partLot.expiration_date > CURRENT_DATE())
)', $identifier ?? 'amountSumLessThanDesired', $default_value);
}
public function apply(QueryBuilder $queryBuilder): void
@ -41,9 +48,9 @@ class LessThanDesiredConstraint extends BooleanConstraint
//If value is true, we want to filter for parts with stock < desired stock
if ($this->value) {
$queryBuilder->andHaving('amountSum < minamount');
$queryBuilder->andHaving( $this->property . ' < part.minamount');
} else {
$queryBuilder->andHaving('amountSum >= minamount');
$queryBuilder->andHaving($this->property . ' >= part.minamount');
}
}
}

View file

@ -30,16 +30,9 @@ class TagsConstraint extends AbstractConstraint
{
final public const ALLOWED_OPERATOR_VALUES = ['ANY', 'ALL', 'NONE'];
/**
* @param string $value
*/
public function __construct(string $property, string $identifier = null, /**
* @var string The value to compare to
*/
protected $value = null, /**
* @var string|null The operator to use
*/
protected ?string $operator = '')
public function __construct(string $property, string $identifier = null,
protected ?string $value = null,
protected ?string $operator = '')
{
parent::__construct($property, $identifier);
}
@ -61,12 +54,12 @@ class TagsConstraint extends AbstractConstraint
return $this;
}
public function getValue(): string
public function getValue(): ?string
{
return $this->value;
}
public function setValue(string $value): self
public function setValue(?string $value): self
{
$this->value = $value;
return $this;

View file

@ -33,9 +33,9 @@ class TextConstraint extends AbstractConstraint
* @param string $value
*/
public function __construct(string $property, string $identifier = null, /**
* @var string The value to compare to
* @var string|null The value to compare to
*/
protected $value = null, /**
protected ?string $value = null, /**
* @var string|null The operator to use
*/
protected ?string $operator = '')
@ -60,12 +60,12 @@ class TextConstraint extends AbstractConstraint
return $this;
}
public function getValue(): string
public function getValue(): ?string
{
return $this->value;
}
public function setValue(string $value): self
public function setValue(?string $value): self
{
$this->value = $value;
return $this;
@ -113,7 +113,7 @@ class TextConstraint extends AbstractConstraint
//Regex is only supported on MySQL and needs a special function
if ($this->operator === 'REGEX') {
$queryBuilder->andWhere(sprintf('REGEXP(%s, :%s) = 1', $this->property, $this->identifier));
$queryBuilder->andWhere(sprintf('REGEXP(%s, :%s) = TRUE', $this->property, $this->identifier));
$queryBuilder->setParameter($this->identifier, $this->value);
}
}

View file

@ -37,6 +37,7 @@ use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier;
use App\Entity\ProjectSystem\Project;
@ -123,8 +124,13 @@ class PartFilter implements FilterInterface
This seems to be related to the fact, that PDO does not have an float parameter type and using string type does not work in this situation (at least in SQLite)
TODO: Find a better solution here
*/
//We have to use Having here, as we use an alias column which is not supported on the where clause and would result in an error
$this->amountSum = (new IntConstraint('amountSum'))->useHaving();
$this->amountSum = (new IntConstraint('(
SELECT COALESCE(SUM(__partLot.amount), 0.0)
FROM '.PartLot::class.' __partLot
WHERE __partLot.part = part.id
AND __partLot.instock_unknown = false
AND (__partLot.expiration_date IS NULL OR __partLot.expiration_date > CURRENT_DATE())
)', identifier: "amountSumWhere"));
$this->lotCount = new IntConstraint('COUNT(_partLots)');
$this->lessThanDesired = new LessThanDesiredConstraint();

View file

@ -129,7 +129,7 @@ class PartSearchFilter implements FilterInterface
//Convert the fields to search to a list of expressions
$expressions = array_map(function (string $field): string {
if ($this->regex) {
return sprintf("REGEXP(%s, :search_query) = 1", $field);
return sprintf("REGEXP(%s, :search_query) = TRUE", $field);
}
return sprintf("%s LIKE :search_query", $field);

View file

@ -109,7 +109,7 @@ class ColumnSortHelper
}
//and the remaining non-visible columns
foreach ($this->columns as $col_id => $col_data) {
foreach (array_keys($this->columns) as $col_id) {
if (in_array($col_id, $processed_columns, true)) {
// column already processed
continue;

View file

@ -154,7 +154,7 @@ class LogDataTable implements DataTableTypeInterface
$dataTable->add('user', TextColumn::class, [
'label' => 'log.user',
'orderField' => 'user.name',
'orderField' => 'NATSORT(user.name)',
'render' => function ($value, AbstractLogEntry $context): string {
$user = $context->getUser();
@ -162,7 +162,7 @@ class LogDataTable implements DataTableTypeInterface
if (!$user instanceof User) {
if ($context->isCLIEntry()) {
return sprintf('%s [%s]',
htmlentities($context->getCLIUsername()),
htmlentities((string) $context->getCLIUsername()),
$this->translator->trans('log.cli_user')
);
}

View file

@ -108,12 +108,14 @@ final class PartsDataTable implements DataTableTypeInterface
->add('name', TextColumn::class, [
'label' => $this->translator->trans('part.table.name'),
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderName($context),
'orderField' => 'NATSORT(part.name)'
])
->add('id', TextColumn::class, [
'label' => $this->translator->trans('part.table.id'),
])
->add('ipn', TextColumn::class, [
'label' => $this->translator->trans('part.table.ipn'),
'orderField' => 'NATSORT(part.ipn)'
])
->add('description', MarkdownColumn::class, [
'label' => $this->translator->trans('part.table.description'),
@ -121,23 +123,24 @@ final class PartsDataTable implements DataTableTypeInterface
->add('category', EntityColumn::class, [
'label' => $this->translator->trans('part.table.category'),
'property' => 'category',
'orderField' => '_category.name'
'orderField' => 'NATSORT(_category.name)'
])
->add('footprint', EntityColumn::class, [
'property' => 'footprint',
'label' => $this->translator->trans('part.table.footprint'),
'orderField' => '_footprint.name'
'orderField' => 'NATSORT(_footprint.name)'
])
->add('manufacturer', EntityColumn::class, [
'property' => 'manufacturer',
'label' => $this->translator->trans('part.table.manufacturer'),
'orderField' => '_manufacturer.name'
'orderField' => 'NATSORT(_manufacturer.name)'
])
->add('storelocation', TextColumn::class, [
'label' => $this->translator->trans('part.table.storeLocations'),
'orderField' => '_storelocations.name',
'orderField' => 'NATSORT(_storelocations.name)',
'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context),
], alias: 'storage_location')
->add('amount', TextColumn::class, [
'label' => $this->translator->trans('part.table.amount'),
'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderAmount($context),
@ -149,9 +152,21 @@ final class PartsDataTable implements DataTableTypeInterface
$context->getPartUnit())),
])
->add('partUnit', TextColumn::class, [
'field' => 'partUnit.name',
'label' => $this->translator->trans('part.table.partUnit'),
'orderField' => '_partUnit.name'
'orderField' => 'NATSORT(_partUnit.name)',
'render' => function($value, Part $context): string {
$partUnit = $context->getPartUnit();
if ($partUnit === null) {
return '';
}
$tmp = htmlspecialchars($partUnit->getName());
if ($partUnit->getUnit()) {
$tmp .= ' ('.htmlspecialchars($partUnit->getUnit()).')';
}
return $tmp;
}
])
->add('addedDate', LocaleDateTimeColumn::class, [
'label' => $this->translator->trans('part.table.addedDate'),
@ -169,7 +184,7 @@ final class PartsDataTable implements DataTableTypeInterface
'label' => $this->translator->trans('part.table.manufacturingStatus'),
'class' => ManufacturingStatus::class,
'render' => function (?ManufacturingStatus $status, Part $context): string {
if (!$status) {
if ($status === null) {
return '';
}
@ -178,6 +193,7 @@ final class PartsDataTable implements DataTableTypeInterface
])
->add('manufacturer_product_number', TextColumn::class, [
'label' => $this->translator->trans('part.table.mpn'),
'orderField' => 'NATSORT(part.manufacturer_product_number)'
])
->add('mass', SIUnitNumberColumn::class, [
'label' => $this->translator->trans('part.table.mass'),
@ -264,8 +280,8 @@ final class PartsDataTable implements DataTableTypeInterface
->addSelect('part.minamount AS HIDDEN minamount')
->from(Part::class, 'part')
//This must be the only group by, or the paginator will not work correctly
->addGroupBy('part.id');
//The other group by fields, are dynamically added by the addJoins method
->addGroupBy('part');
}
private function getDetailQuery(QueryBuilder $builder, array $filter_results): void
@ -345,7 +361,7 @@ final class PartsDataTable implements DataTableTypeInterface
//Calculate amount sum using a subquery, so we can filter and sort by it
$builder->addSelect(
'(
SELECT IFNULL(SUM(partLot.amount), 0.0)
SELECT COALESCE(SUM(partLot.amount), 0.0)
FROM '.PartLot::class.' partLot
WHERE partLot.part = part.id
AND partLot.instock_unknown = false
@ -356,35 +372,52 @@ final class PartsDataTable implements DataTableTypeInterface
if (str_contains($dql, '_category')) {
$builder->leftJoin('part.category', '_category');
$builder->addGroupBy('_category');
}
if (str_contains($dql, '_master_picture_attachment')) {
$builder->leftJoin('part.master_picture_attachment', '_master_picture_attachment');
$builder->addGroupBy('_master_picture_attachment');
}
if (str_contains($dql, '_partLots') || str_contains($dql, '_storelocations')) {
$builder->leftJoin('part.partLots', '_partLots');
$builder->leftJoin('_partLots.storage_location', '_storelocations');
//Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1
//$builder->addGroupBy('_partLots');
//$builder->addGroupBy('_storelocations');
}
if (str_contains($dql, '_footprint')) {
$builder->leftJoin('part.footprint', '_footprint');
$builder->addGroupBy('_footprint');
}
if (str_contains($dql, '_manufacturer')) {
$builder->leftJoin('part.manufacturer', '_manufacturer');
$builder->addGroupBy('_manufacturer');
}
if (str_contains($dql, '_orderdetails') || str_contains($dql, '_suppliers')) {
$builder->leftJoin('part.orderdetails', '_orderdetails');
$builder->leftJoin('_orderdetails.supplier', '_suppliers');
//Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1
//$builder->addGroupBy('_orderdetails');
//$builder->addGroupBy('_suppliers');
}
if (str_contains($dql, '_attachments')) {
$builder->leftJoin('part.attachments', '_attachments');
//Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1
//$builder->addGroupBy('_attachments');
}
if (str_contains($dql, '_partUnit')) {
$builder->leftJoin('part.partUnit', '_partUnit');
$builder->addGroupBy('_partUnit');
}
if (str_contains($dql, '_parameters')) {
$builder->leftJoin('part.parameters', '_parameters');
//Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1
//$builder->addGroupBy('_parameters');
}
if (str_contains($dql, '_projectBomEntries')) {
$builder->leftJoin('part.project_bom_entries', '_projectBomEntries');
//Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1
//$builder->addGroupBy('_projectBomEntries');
}
return $builder;

View file

@ -82,10 +82,10 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
->add('name', TextColumn::class, [
'label' => $this->translator->trans('part.table.name'),
'orderField' => 'part.name',
'orderField' => 'NATSORT(part.name)',
'render' => function ($value, ProjectBOMEntry $context) {
if(!$context->getPart() instanceof Part) {
return htmlspecialchars($context->getName());
return htmlspecialchars((string) $context->getName());
}
if($context->getPart() instanceof Part) {
$tmp = $this->partDataTableHelper->renderName($context->getPart());
@ -101,7 +101,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
])
->add('ipn', TextColumn::class, [
'label' => $this->translator->trans('part.table.ipn'),
'orderField' => 'part.ipn',
'orderField' => 'NATSORT(part.ipn)',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
if($context->getPart() instanceof Part) {
@ -124,18 +124,18 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
->add('category', EntityColumn::class, [
'label' => $this->translator->trans('part.table.category'),
'property' => 'part.category',
'orderField' => 'category.name',
'orderField' => 'NATSORT(category.name)',
])
->add('footprint', EntityColumn::class, [
'property' => 'part.footprint',
'label' => $this->translator->trans('part.table.footprint'),
'orderField' => 'footprint.name',
'orderField' => 'NATSORT(footprint.name)',
])
->add('manufacturer', EntityColumn::class, [
'property' => 'part.manufacturer',
'label' => $this->translator->trans('part.table.manufacturer'),
'orderField' => 'manufacturer.name',
'orderField' => 'NATSORT(manufacturer.name)',
])
->add('mountnames', TextColumn::class, [
@ -154,7 +154,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
'label' => 'project.bom.instockAmount',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
if ($context->getPart()) {
if ($context->getPart() !== null) {
return $this->partDataTableHelper->renderAmount($context->getPart());
}
@ -165,7 +165,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
'label' => 'part.table.storeLocations',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
if ($context->getPart()) {
if ($context->getPart() !== null) {
return $this->partDataTableHelper->renderStorageLocations($context->getPart());
}

View file

@ -0,0 +1,59 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 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\Doctrine\Functions;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\Node;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\TokenType;
class ArrayPosition extends FunctionNode
{
private ?Node $array = null;
private ?Node $field = null;
public function parse(Parser $parser): void
{
$parser->match(TokenType::T_IDENTIFIER);
$parser->match(TokenType::T_OPEN_PARENTHESIS);
$this->array = $parser->InParameter();
$parser->match(TokenType::T_COMMA);
$this->field = $parser->ArithmeticPrimary();
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
}
public function getSql(SqlWalker $sqlWalker): string
{
return 'ARRAY_POSITION(' .
$this->array->dispatch($sqlWalker) . ', ' .
$this->field->dispatch($sqlWalker) .
')';
}
}

View file

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace App\Doctrine\Functions;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\TokenType;
@ -36,7 +38,7 @@ class Field2 extends FunctionNode
private $values = [];
public function parse(\Doctrine\ORM\Query\Parser $parser): void
public function parse(Parser $parser): void
{
$parser->match(TokenType::T_IDENTIFIER);
$parser->match(TokenType::T_OPEN_PARENTHESIS);
@ -50,7 +52,7 @@ class Field2 extends FunctionNode
$lexer = $parser->getLexer();
while (count($this->values) < 1 ||
$lexer->lookahead['type'] != TokenType::T_CLOSE_PARENTHESIS) {
$lexer->lookahead->type !== TokenType::T_CLOSE_PARENTHESIS) {
$parser->match(TokenType::T_COMMA);
$this->values[] = $parser->ArithmeticPrimary();
}
@ -58,15 +60,16 @@ class Field2 extends FunctionNode
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker): string
public function getSql(SqlWalker $sqlWalker): string
{
$query = 'FIELD2(';
$query .= $this->field->dispatch($sqlWalker);
$query .= ', ';
$counter = count($this->values);
for ($i = 0; $i < count($this->values); $i++) {
for ($i = 0; $i < $counter; $i++) {
if ($i > 0) {
$query .= ', ';
}
@ -74,8 +77,6 @@ class Field2 extends FunctionNode
$query .= $this->values[$i]->dispatch($sqlWalker);
}
$query .= ')';
return $query;
return $query . ')';
}
}

View file

@ -0,0 +1,142 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 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\Doctrine\Functions;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\MariaDBPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\Node;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\TokenType;
class Natsort extends FunctionNode
{
private ?Node $field = null;
private static ?bool $supportsNaturalSort = null;
private static bool $allowSlowNaturalSort = false;
/**
* As we can not inject parameters into the function, we use an event listener, to call the value on the static function.
* This is the only way to inject the value into the function.
* @param bool $allow
* @return void
*/
public static function allowSlowNaturalSort(bool $allow = true): void
{
self::$allowSlowNaturalSort = $allow;
}
/**
* Check if the MariaDB version which is connected to supports the natural sort (meaning it has a version of 10.7.0 or higher)
* The result is cached in memory.
* @param Connection $connection
* @return bool
* @throws Exception
*/
private function mariaDBSupportsNaturalSort(Connection $connection): bool
{
if (self::$supportsNaturalSort !== null) {
return self::$supportsNaturalSort;
}
$version = $connection->getServerVersion();
//Get the effective MariaDB version number
$version = $this->getMariaDbMysqlVersionNumber($version);
//We need at least MariaDB 10.7.0 to support the natural sort
self::$supportsNaturalSort = version_compare($version, '10.7.0', '>=');
return self::$supportsNaturalSort;
}
/**
* Taken from Doctrine\DBAL\Driver\AbstractMySQLDriver
*
* Detect MariaDB server version, including hack for some mariadb distributions
* that starts with the prefix '5.5.5-'
*
* @param string $versionString Version string as returned by mariadb server, i.e. '5.5.5-Mariadb-10.0.8-xenial'
*/
private function getMariaDbMysqlVersionNumber(string $versionString) : string
{
if ( ! preg_match(
'/^(?:5\.5\.5-)?(mariadb-)?(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)/i',
$versionString,
$versionParts
)) {
throw new \RuntimeException('Could not detect MariaDB version from version string ' . $versionString);
}
return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch'];
}
public function parse(Parser $parser): void
{
$parser->match(TokenType::T_IDENTIFIER);
$parser->match(TokenType::T_OPEN_PARENTHESIS);
$this->field = $parser->ArithmeticExpression();
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
}
public function getSql(SqlWalker $sqlWalker): string
{
assert($this->field !== null, 'Field is not set');
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
if ($platform instanceof PostgreSQLPlatform) {
return $this->field->dispatch($sqlWalker) . ' COLLATE numeric';
}
if ($platform instanceof MariaDBPlatform && $this->mariaDBSupportsNaturalSort($sqlWalker->getConnection())) {
return 'NATURAL_SORT_KEY(' . $this->field->dispatch($sqlWalker) . ')';
}
//Do the following operations only if we allow slow natural sort
if (self::$allowSlowNaturalSort) {
if ($platform instanceof SQLitePlatform) {
return $this->field->dispatch($sqlWalker).' COLLATE NATURAL_CMP';
}
if ($platform instanceof AbstractMySQLPlatform) {
return 'NatSortKey(' . $this->field->dispatch($sqlWalker) . ', 0)';
}
}
//For every other platform, return the field as is
return $this->field->dispatch($sqlWalker);
}
}

View file

@ -0,0 +1,52 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 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\Doctrine\Functions;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\ORM\Query\SqlWalker;
/**
* Similar to the regexp function, but with support for multi platform.
*/
class Regexp extends \DoctrineExtensions\Query\Mysql\Regexp
{
public function getSql(SqlWalker $sqlWalker): string
{
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
//
if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) {
$operator = 'REGEXP';
} elseif ($platform instanceof PostgreSQLPlatform) {
//Use the case-insensitive operator, to have the same behavior as MySQL
$operator = '~*';
} else {
throw new \RuntimeException('Platform ' . gettype($platform) . ' does not support regular expressions.');
}
return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->regexp->dispatch($sqlWalker) . ')';
}
}

View file

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace App\Doctrine\Helpers;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\ORM\QueryBuilder;
/**
@ -45,10 +46,10 @@ final class FieldHelper
$db_platform = $qb->getEntityManager()->getConnection()->getDatabasePlatform();
//If we are on MySQL, we can just use the FIELD function
if ($db_platform instanceof AbstractMySQLPlatform) {
$param = (is_numeric($bound_param) ? '?' : ":") . (string) $bound_param;
if ($db_platform instanceof AbstractMySQLPlatform ) {
$param = (is_numeric($bound_param) ? '?' : ":").(string)$bound_param;
$qb->orderBy("FIELD($field_expr, $param)", $order);
} else {
} else { //Use the sqlite/portable version or postgresql
//Retrieve the values from the bound parameter
$param = $qb->getParameter($bound_param);
if ($param === null) {
@ -57,12 +58,31 @@ final class FieldHelper
//Generate a unique key from the field_expr
$key = 'field2_' . (string) $bound_param;
self::addSqliteOrderBy($qb, $field_expr, $key, $param->getValue(), $order);
if ($db_platform instanceof PostgreSQLPlatform) {
self::addPostgresOrderBy($qb, $field_expr, $key, $param->getValue(), $order);
} else {
self::addSqliteOrderBy($qb, $field_expr, $key, $param->getValue(), $order);
}
}
return $qb;
}
private static function addPostgresOrderBy(QueryBuilder $qb, string $field_expr, string $key, array $values, ?string $order = null): void
{
//Use postgres native array_position function, to get the index of the value in the array
//In the end it gives a similar result as the FIELD function
$qb->orderBy("array_position(:$key, $field_expr)", $order);
//Convert the values to a literal array, to overcome the problem of passing more than 100 parameters
$values = array_map(fn($value) => is_string($value) ? "'$value'" : $value, $values);
$literalArray = '{' . implode(',', $values) . '}';
$qb->setParameter($key, $literalArray);
}
private static function addSqliteOrderBy(QueryBuilder $qb, string $field_expr, string $key, array $values, ?string $order = null): void
{
//Otherwise we emulate it using
@ -88,11 +108,12 @@ final class FieldHelper
//If we are on MySQL, we can just use the FIELD function
if ($db_platform instanceof AbstractMySQLPlatform) {
$qb->orderBy("FIELD($field_expr, :field_arr)", $order);
$qb->orderBy("FIELD2($field_expr, :field_arr)", $order);
} elseif ($db_platform instanceof PostgreSQLPlatform) {
//Use the postgres native array_position function
self::addPostgresOrderBy($qb, $field_expr, $key, $values, $order);
} else {
//Generate a unique key from the field_expr
//Otherwise we have to it using the FIELD2 function
//Otherwise use the portable version using string concatenation
self::addSqliteOrderBy($qb, $field_expr, $key, $values, $order);
}

View file

@ -27,7 +27,6 @@ use Composer\CaBundle\CaBundle;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
/**
* This middleware sets SSL options for MySQL connections
@ -42,7 +41,7 @@ class MySQLSSLConnectionMiddlewareDriver extends AbstractDriverMiddleware
public function connect(array $params): Connection
{
//Only set this on MySQL connections, as other databases don't support this parameter
if($this->enabled && $this->getDatabasePlatform() instanceof AbstractMySQLPlatform) {
if($this->enabled && $params['driver'] === 'pdo_mysql') {
$params['driverOptions'][\PDO::MYSQL_ATTR_SSL_CA] = CaBundle::getSystemCaRootBundlePath();
$params['driverOptions'][\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $this->verify;
}

View file

@ -26,7 +26,6 @@ namespace App\Doctrine\Middleware;
use App\Exceptions\InvalidRegexException;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use Doctrine\DBAL\Platforms\SqlitePlatform;
/**
* This middleware is used to add the regexp operator to the SQLite platform.
@ -41,7 +40,7 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
$connection = parent::connect($params); // TODO: Change the autogenerated stub
//Then add the functions if we are on SQLite
if ($this->getDatabasePlatform() instanceof SqlitePlatform) {
if ($params['driver'] === 'pdo_sqlite') {
$native_connection = $connection->getNativeConnection();
//Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation
@ -49,6 +48,11 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
$native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC);
$native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC);
$native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC);
//Create a new collation for natural sorting
if (method_exists($native_connection, 'sqliteCreateCollation')) {
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
}
}
}
@ -94,10 +98,9 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
* This function returns the index (position) of the first argument in the subsequent arguments.
* If the first argument is not found or is NULL, 0 is returned.
* @param string|int|null $value
* @param mixed ...$array
* @return int
*/
final public static function field(string|int|null $value, ...$array): int
final public static function field(string|int|null $value, mixed ...$array): int
{
if ($value === null) {
return 0;

View file

@ -24,7 +24,6 @@ namespace App\Doctrine\Middleware;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
/**
* This command sets the initial command parameter for MySQL connections, so we can set the SQL mode
@ -35,7 +34,7 @@ class SetSQLModeMiddlewareDriver extends AbstractDriverMiddleware
public function connect(array $params): Connection
{
//Only set this on MySQL connections, as other databases don't support this parameter
if($this->getDatabasePlatform() instanceof AbstractMySQLPlatform) {
if($params['driver'] === 'pdo_mysql') {
//1002 is \PDO::MYSQL_ATTR_INIT_COMMAND constant value
$params['driverOptions'][\PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))';
}

View file

@ -31,8 +31,6 @@ use Doctrine\DBAL\Schema\Identifier;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use function array_reverse;
use function assert;
use function count;
@ -207,6 +205,8 @@ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface
return 'ALTER TABLE '.$tableIdentifier->getQuotedName($platform).' AUTO_INCREMENT = 1;';
}
throw new \RuntimeException("Resetting autoincrement is not supported on this platform!");
//This seems to cause problems somehow
/*if ($platform instanceof SqlitePlatform) {
return 'DELETE FROM `sqlite_sequence` WHERE name = \''.$tableIdentifier->getQuotedName($platform).'\';';
@ -278,7 +278,7 @@ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface
foreach ($classes as $class) {
foreach ($class->associationMappings as $assoc) {
if (! $assoc['isOwningSide'] || $assoc['type'] !== ClassMetadataInfo::MANY_TO_MANY) {
if (! $assoc['isOwningSide'] || $assoc['type'] !== ClassMetadata::MANY_TO_MANY) {
continue;
}

View file

@ -0,0 +1,116 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 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\Doctrine\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Exception\SerializationFailed;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use function is_resource;
use function restore_error_handler;
use function serialize;
use function set_error_handler;
use function stream_get_contents;
use function unserialize;
use const E_DEPRECATED;
use const E_USER_DEPRECATED;
/**
* This class is taken from doctrine ORM 3.8. https://github.com/doctrine/dbal/blob/3.8.x/src/Types/ArrayType.php
*
* It was removed in doctrine ORM 4.0. However, we require it for backward compatibility with WebauthnKey.
* Therefore, we manually added it here as a custom type as a forward compatibility layer.
*/
class ArrayType extends Type
{
/**
* {@inheritDoc}
*/
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
return $platform->getClobTypeDeclarationSQL($column);
}
/**
* {@inheritDoc}
*/
public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): string
{
return serialize($value);
}
/**
* {@inheritDoc}
*/
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed
{
if ($value === null) {
return null;
}
$value = is_resource($value) ? stream_get_contents($value) : $value;
set_error_handler(function (int $code, string $message): bool {
if ($code === E_DEPRECATED || $code === E_USER_DEPRECATED) {
return false;
}
//Change to original code. Use SerializationFailed instead of ConversionException.
throw new SerializationFailed("Serialization failed (Code $code): " . $message);
});
try {
//Change to original code. Use false for allowed_classes, to avoid unsafe unserialization of objects.
return unserialize($value, ['allowed_classes' => false]);
} finally {
restore_error_handler();
}
}
/**
* {@inheritDoc}
*/
public function getName(): string
{
return "array";
}
/**
* {@inheritDoc}
*
* @deprecated
*/
public function requiresSQLCommentHint(AbstractPlatform $platform): bool
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5509',
'%s is deprecated.',
__METHOD__,
);
return true;
}
}

View file

@ -22,7 +22,9 @@ declare(strict_types=1);
*/
namespace App\Doctrine\Types;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\DBAL\Types\Type;
/**
@ -33,7 +35,15 @@ class TinyIntType extends Type
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
return 'TINYINT';
//MySQL knows the TINYINT type directly
//We do not use the TINYINT for sqlite, as it will be resolved to a BOOL type and bring problems with migrations
if ($platform instanceof AbstractMySQLPlatform ) {
//Use TINYINT(1) to allow for proper migration diffs
return 'TINYINT(1)';
}
//For other platforms, we use the smallest integer type available
return $platform->getSmallIntTypeDeclarationSQL($column);
}
public function getName(): string

View file

@ -0,0 +1,97 @@
<?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);
namespace App\Doctrine\Types;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\DateTimeImmutableType;
use Doctrine\DBAL\Types\DateTimeType;
use Doctrine\DBAL\Types\Exception\InvalidFormat;
/**
* This DateTimeImmutableType all dates to UTC, so it can be later used with the timezones.
* Taken (and adapted) from here: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/cookbook/working-with-datetime.html.
*/
class UTCDateTimeImmutableType extends DateTimeImmutableType
{
private static ?DateTimeZone $utc_timezone = null;
/**
* {@inheritdoc}
*
* @param T $value
*
* @return (T is null ? null : string)
*
* @template T
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
{
if (!self::$utc_timezone instanceof \DateTimeZone) {
self::$utc_timezone = new DateTimeZone('UTC');
}
if ($value instanceof \DateTimeImmutable) {
$value = $value->setTimezone(self::$utc_timezone);
}
return parent::convertToDatabaseValue($value, $platform);
}
/**
* {@inheritDoc}
*
* @param T $value
*
* @template T
*/
public function convertToPHPValue($value, AbstractPlatform $platform): ?\DateTimeImmutable
{
if (!self::$utc_timezone instanceof \DateTimeZone) {
self::$utc_timezone = new DateTimeZone('UTC');
}
if (null === $value || $value instanceof \DateTimeImmutable) {
return $value;
}
$converted = \DateTimeImmutable::createFromFormat(
$platform->getDateTimeFormatString(),
$value,
self::$utc_timezone
);
if (!$converted) {
throw InvalidFormat::new(
$value,
static::class,
$platform->getDateTimeFormatString(),
);
}
return $converted;
}
}

View file

@ -28,6 +28,7 @@ use DateTimeZone;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\DateTimeType;
use Doctrine\DBAL\Types\Exception\InvalidFormat;
/**
* This DateTimeType all dates to UTC, so it can be later used with the timezones.
@ -64,11 +65,9 @@ class UTCDateTimeType extends DateTimeType
*
* @param T $value
*
* @return (T is null ? null : DateTimeInterface)
*
* @template T
*/
public function convertToPHPValue($value, AbstractPlatform $platform): ?\DateTimeInterface
public function convertToPHPValue($value, AbstractPlatform $platform): ?DateTime
{
if (!self::$utc_timezone instanceof \DateTimeZone) {
self::$utc_timezone = new DateTimeZone('UTC');
@ -85,7 +84,11 @@ class UTCDateTimeType extends DateTimeType
);
if (!$converted) {
throw ConversionException::conversionFailedFormat($value, $this->getName(), $platform->getDateTimeFormatString());
throw InvalidFormat::new(
$value,
static::class,
$platform->getDateTimeFormatString(),
);
}
return $converted;

View file

@ -147,7 +147,7 @@ abstract class Attachment extends AbstractNamedDBElement
* @var string|null the original filename the file had, when the user uploaded it
*/
#[ORM\Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'attachment:read'])]
#[Groups(['attachment:read', 'import'])]
#[Assert\Length(max: 255)]
protected ?string $original_filename = null;
@ -161,7 +161,7 @@ abstract class Attachment extends AbstractNamedDBElement
* @var string the name of this element
*/
#[Assert\NotBlank(message: 'validator.attachment.name_not_blank')]
#[Groups(['simple', 'extended', 'full', 'attachment:read', 'attachment:write'])]
#[Groups(['simple', 'extended', 'full', 'attachment:read', 'attachment:write', 'import'])]
protected string $name = '';
/**
@ -173,21 +173,21 @@ abstract class Attachment extends AbstractNamedDBElement
protected ?AttachmentContainingDBElement $element = null;
#[ORM\Column(type: Types::BOOLEAN)]
#[Groups(['attachment:read', 'attachment_write'])]
#[Groups(['attachment:read', 'attachment_write', 'full', 'import'])]
protected bool $show_in_table = false;
#[Assert\NotNull(message: 'validator.attachment.must_not_be_null')]
#[ORM\ManyToOne(targetEntity: AttachmentType::class, inversedBy: 'attachments_with_type')]
#[ORM\JoinColumn(name: 'type_id', nullable: false)]
#[Selectable]
#[Groups(['attachment:read', 'attachment:write'])]
#[Groups(['attachment:read', 'attachment:write', 'import', 'full'])]
#[ApiProperty(readableLink: false)]
protected ?AttachmentType $attachment_type = null;
#[Groups(['attachment:read'])]
protected ?\DateTimeInterface $addedDate = null;
protected ?\DateTimeImmutable $addedDate = null;
#[Groups(['attachment:read'])]
protected ?\DateTimeInterface $lastModified = null;
protected ?\DateTimeImmutable $lastModified = null;
public function __construct()
@ -385,7 +385,7 @@ abstract class Attachment extends AbstractNamedDBElement
return null;
}
return parse_url($this->getURL(), PHP_URL_HOST);
return parse_url((string) $this->getURL(), PHP_URL_HOST);
}
/**
@ -477,7 +477,8 @@ abstract class Attachment extends AbstractNamedDBElement
*/
public function setElement(AttachmentContainingDBElement $element): self
{
if (!is_a($element, static::ALLOWED_ELEMENT_CLASS)) {
//Do not allow Rector to replace this check with a instanceof. It will not work!!
if (!is_a($element, static::ALLOWED_ELEMENT_CLASS, true)) {
throw new InvalidArgumentException(sprintf('The element associated with a %s must be a %s!', static::class, static::ALLOWED_ELEMENT_CLASS));
}

View file

@ -45,7 +45,7 @@ abstract class AttachmentContainingDBElement extends AbstractNamedDBElement impl
* @phpstan-var Collection<int, AT>
* ORM Mapping is done in subclasses (e.g. Part)
*/
#[Groups(['full'])]
#[Groups(['full', 'import'])]
protected Collection $attachments;
public function __construct()

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Attachments;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
@ -86,7 +87,7 @@ use Symfony\Component\Validator\Constraints as Assert;
class AttachmentType extends AbstractStructuralDBElement
{
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: AttachmentType::class, cascade: ['persist'])]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $children;
#[ORM\ManyToOne(targetEntity: AttachmentType::class, inversedBy: 'children')]
@ -102,7 +103,7 @@ class AttachmentType extends AbstractStructuralDBElement
*/
#[ORM\Column(type: Types::TEXT)]
#[ValidFileFilter]
#[Groups(['attachment_type:read', 'attachment_type:write'])]
#[Groups(['attachment_type:read', 'attachment_type:write', 'import', 'extended'])]
protected string $filetype_filter = '';
/**
@ -110,21 +111,21 @@ class AttachmentType extends AbstractStructuralDBElement
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: AttachmentTypeAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[Groups(['attachment_type:read', 'attachment_type:write'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
#[Groups(['attachment_type:read', 'attachment_type:write', 'import', 'full'])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: AttachmentTypeAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
#[Groups(['attachment_type:read', 'attachment_type:write'])]
#[Groups(['attachment_type:read', 'attachment_type:write', 'full'])]
protected ?Attachment $master_picture_attachment = null;
/** @var Collection<int, AttachmentTypeParameter>
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: AttachmentTypeParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
#[Groups(['attachment_type:read', 'attachment_type:write'])]
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
#[Groups(['attachment_type:read', 'attachment_type:write', 'import', 'full'])]
protected Collection $parameters;
/**
@ -134,9 +135,9 @@ class AttachmentType extends AbstractStructuralDBElement
protected Collection $attachments_with_type;
#[Groups(['attachment_type:read'])]
protected ?\DateTimeInterface $addedDate = null;
protected ?\DateTimeImmutable $addedDate = null;
#[Groups(['attachment_type:read'])]
protected ?\DateTimeInterface $lastModified = null;
protected ?\DateTimeImmutable $lastModified = null;
public function __construct()

View file

@ -41,14 +41,14 @@ use Symfony\Component\Validator\Constraints as Assert;
abstract class AbstractCompany extends AbstractPartsContainingDBElement
{
#[Groups(['company:read'])]
protected ?\DateTimeInterface $addedDate = null;
protected ?\DateTimeImmutable $addedDate = null;
#[Groups(['company:read'])]
protected ?\DateTimeInterface $lastModified = null;
protected ?\DateTimeImmutable $lastModified = null;
/**
* @var string The address of the company
*/
#[Groups(['full', 'company:read', 'company:write'])]
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
#[ORM\Column(type: Types::STRING)]
#[Assert\Length(max: 255)]
protected string $address = '';
@ -56,7 +56,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
/**
* @var string The phone number of the company
*/
#[Groups(['full', 'company:read', 'company:write'])]
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
#[ORM\Column(type: Types::STRING)]
#[Assert\Length(max: 255)]
protected string $phone_number = '';
@ -64,7 +64,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
/**
* @var string The fax number of the company
*/
#[Groups(['full', 'company:read', 'company:write'])]
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
#[ORM\Column(type: Types::STRING)]
#[Assert\Length(max: 255)]
protected string $fax_number = '';
@ -73,7 +73,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
* @var string The email address of the company
*/
#[Assert\Email]
#[Groups(['full', 'company:read', 'company:write'])]
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
#[ORM\Column(type: Types::STRING)]
#[Assert\Length(max: 255)]
protected string $email_address = '';
@ -82,12 +82,12 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
* @var string The website of the company
*/
#[Assert\Url]
#[Groups(['full', 'company:read', 'company:write'])]
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
#[ORM\Column(type: Types::STRING)]
#[Assert\Length(max: 255)]
protected string $website = '';
#[Groups(['company:read', 'company:write'])]
#[Groups(['company:read', 'company:write', 'import', 'full', 'extended'])]
protected string $comment = '';
/**
@ -95,6 +95,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
*/
#[ORM\Column(type: Types::STRING)]
#[Assert\Length(max: 255)]
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
protected string $auto_product_url = '';
/********************************************************************************

View file

@ -38,7 +38,7 @@ use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\MappedSuperclass(repositoryClass: AbstractPartsContainingRepository::class)]
abstract class AbstractPartsContainingDBElement extends AbstractStructuralDBElement
{
#[Groups(['full'])]
#[Groups(['full', 'import'])]
protected Collection $parameters;
public function __construct()

View file

@ -30,11 +30,11 @@ interface PartsContainingRepositoryInterface
* Returns all parts associated with this element.
*
* @param object $element the element for which the parts should be determined
* @param array $order_by The order of the parts. Format ['name' => 'ASC']
* @param string $nameOrderDirection the direction in which the parts should be ordered by name, either ASC or DESC
*
* @return Part[]
*/
public function getParts(object $element, array $order_by = ['name' => 'ASC']): array;
public function getParts(object $element, string $nameOrderDirection = "ASC"): array;
/**
* Gets the count of the parts associated with this element.

View file

@ -34,28 +34,28 @@ use Symfony\Component\Serializer\Annotation\Groups;
trait TimestampTrait
{
/**
* @var \DateTimeInterface|null the date when this element was modified the last time
* @var \DateTimeImmutable|null the date when this element was modified the last time
*/
#[Groups(['extended', 'full'])]
#[ApiProperty(writable: false)]
#[ORM\Column(name: 'last_modified', type: Types::DATETIME_MUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])]
protected ?\DateTimeInterface $lastModified = null;
#[ORM\Column(name: 'last_modified', type: Types::DATETIME_IMMUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])]
protected ?\DateTimeImmutable $lastModified = null;
/**
* @var \DateTimeInterface|null the date when this element was created
* @var \DateTimeImmutable|null the date when this element was created
*/
#[Groups(['extended', 'full'])]
#[ApiProperty(writable: false)]
#[ORM\Column(name: 'datetime_added', type: Types::DATETIME_MUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])]
protected ?\DateTimeInterface $addedDate = null;
#[ORM\Column(name: 'datetime_added', type: Types::DATETIME_IMMUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])]
protected ?\DateTimeImmutable $addedDate = null;
/**
* Returns the last time when the element was modified.
* Returns null if the element was not yet saved to DB yet.
*
* @return \DateTimeInterface|null the time of the last edit
* @return \DateTimeImmutable|null the time of the last edit
*/
public function getLastModified(): ?\DateTimeInterface
public function getLastModified(): ?\DateTimeImmutable
{
return $this->lastModified;
}
@ -64,9 +64,9 @@ trait TimestampTrait
* Returns the date/time when the element was created.
* Returns null if the element was not yet saved to DB yet.
*
* @return \DateTimeInterface|null the creation time of the part
* @return \DateTimeImmutable|null the creation time of the part
*/
public function getAddedDate(): ?\DateTimeInterface
public function getAddedDate(): ?\DateTimeImmutable
{
return $this->addedDate;
}
@ -78,9 +78,9 @@ trait TimestampTrait
#[ORM\PreUpdate]
public function updateTimestamps(): void
{
$this->lastModified = new DateTime('now');
$this->lastModified = new \DateTimeImmutable('now');
if (null === $this->addedDate) {
$this->addedDate = new DateTime('now');
$this->addedDate = new \DateTimeImmutable('now');
}
}
}

View file

@ -36,33 +36,33 @@ class EDACategoryInfo
* @var string|null The reference prefix of the Part in the schematic. E.g. "R" for resistors, or "C" for capacitors.
*/
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'category:read', 'category:write'])]
#[Groups(['full', 'category:read', 'category:write', 'import'])]
#[Length(max: 255)]
private ?string $reference_prefix = null;
/** @var bool|null Visibility of this part to EDA software in trinary logic. True=Visible, False=Invisible, Null=Auto */
#[Column(name: 'invisible', type: Types::BOOLEAN, nullable: true)] //TODO: Rename column to visibility
#[Groups(['full', 'category:read', 'category:write'])]
#[Groups(['full', 'category:read', 'category:write', 'import'])]
private ?bool $visibility = null;
/** @var bool|null If this is set to true, then this part will be excluded from the BOM */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'category:read', 'category:write'])]
#[Groups(['full', 'category:read', 'category:write', 'import'])]
private ?bool $exclude_from_bom = null;
/** @var bool|null If this is set to true, then this part will be excluded from the board/the PCB */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'category:read', 'category:write'])]
#[Groups(['full', 'category:read', 'category:write', 'import'])]
private ?bool $exclude_from_board = null;
/** @var bool|null If this is set to true, then this part will be excluded in the simulation */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'category:read', 'category:write'])]
#[Groups(['full', 'category:read', 'category:write', 'import'])]
private ?bool $exclude_from_sim = true;
/** @var string|null The KiCAD schematic symbol, which should be used (the path to the library) */
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'category:read', 'category:write'])]
#[Groups(['full', 'category:read', 'category:write', 'import'])]
#[Length(max: 255)]
private ?string $kicad_symbol = null;

View file

@ -34,7 +34,7 @@ class EDAFootprintInfo
{
/** @var string|null The KiCAD footprint, which should be used (the path to the library) */
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'footprint:read', 'footprint:write'])]
#[Groups(['full', 'footprint:read', 'footprint:write', 'import'])]
#[Length(max: 255)]
private ?string $kicad_footprint = null;

View file

@ -36,45 +36,45 @@ class EDAPartInfo
* @var string|null The reference prefix of the Part in the schematic. E.g. "R" for resistors, or "C" for capacitors.
*/
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
#[Length(max: 255)]
private ?string $reference_prefix = null;
/** @var string|null The value, which should be shown together with the part (e.g. 470 for a 470 Ohm resistor) */
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
#[Length(max: 255)]
private ?string $value = null;
/** @var bool|null Visibility of this part to EDA software in trinary logic. True=Visible, False=Invisible, Null=Auto */
#[Column(name: 'invisible', type: Types::BOOLEAN, nullable: true)] //TODO: Rename column to visibility
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
private ?bool $visibility = null;
/** @var bool|null If this is set to true, then this part will be excluded from the BOM */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
private ?bool $exclude_from_bom = null;
/** @var bool|null If this is set to true, then this part will be excluded from the board/the PCB */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
private ?bool $exclude_from_board = null;
/** @var bool|null If this is set to true, then this part will be excluded in the simulation */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
private ?bool $exclude_from_sim = null;
/** @var string|null The KiCAD schematic symbol, which should be used (the path to the library) */
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
#[Length(max: 255)]
private ?string $kicad_symbol = null;
/** @var string|null The KiCAD footprint, which should be used (the path to the library) */
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
#[Length(max: 255)]
private ?string $kicad_footprint = null;

View file

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
@ -17,7 +20,6 @@
* 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\Entity\LabelSystem;
enum BarcodeType: string

View file

@ -43,6 +43,7 @@ namespace App\Entity\LabelSystem;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Embeddable]
@ -53,6 +54,7 @@ class LabelOptions
*/
#[Assert\Positive]
#[ORM\Column(type: Types::FLOAT)]
#[Groups(["extended", "full", "import"])]
protected float $width = 50.0;
/**
@ -60,38 +62,45 @@ class LabelOptions
*/
#[Assert\Positive]
#[ORM\Column(type: Types::FLOAT)]
#[Groups(["extended", "full", "import"])]
protected float $height = 30.0;
/**
* @var BarcodeType The type of the barcode that should be used in the label (e.g. 'qr')
*/
#[ORM\Column(type: Types::STRING, enumType: BarcodeType::class)]
#[Groups(["extended", "full", "import"])]
protected BarcodeType $barcode_type = BarcodeType::NONE;
/**
* @var LabelPictureType What image should be shown along the label
*/
#[ORM\Column(type: Types::STRING, enumType: LabelPictureType::class)]
#[Groups(["extended", "full", "import"])]
protected LabelPictureType $picture_type = LabelPictureType::NONE;
#[ORM\Column(type: Types::STRING, enumType: LabelSupportedElement::class)]
#[Groups(["extended", "full", "import"])]
protected LabelSupportedElement $supported_element = LabelSupportedElement::PART;
/**
* @var string any additional CSS for the label
*/
#[ORM\Column(type: Types::TEXT)]
#[Groups([ "full", "import"])]
protected string $additional_css = '';
/** @var LabelProcessMode The mode that will be used to interpret the lines
*/
#[ORM\Column(name: 'lines_mode', type: Types::STRING, enumType: LabelProcessMode::class)]
#[Groups(["extended", "full", "import"])]
protected LabelProcessMode $process_mode = LabelProcessMode::PLACEHOLDER;
/**
* @var string
*/
#[ORM\Column(type: Types::TEXT)]
#[Groups(["extended", "full", "import"])]
protected string $lines = '';
public function getWidth(): float

View file

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
@ -17,7 +20,6 @@
* 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\Entity\LabelSystem;
enum LabelPictureType: string
@ -34,4 +36,4 @@ enum LabelPictureType: string
* Show the main attachment of the element on the label
*/
case MAIN_ATTACHMENT = 'main_attachment';
}
}

View file

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
@ -17,7 +20,6 @@
* 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\Entity\LabelSystem;
enum LabelProcessMode: string
@ -26,4 +28,4 @@ enum LabelProcessMode: string
case PLACEHOLDER = 'html';
/** Interpret the given lines as twig template */
case TWIG = 'twig';
}
}

View file

@ -41,6 +41,7 @@ declare(strict_types=1);
namespace App\Entity\LabelSystem;
use Doctrine\Common\Collections\Criteria;
use App\Entity\Attachments\Attachment;
use App\Repository\LabelProfileRepository;
use App\EntityListeners\TreeCacheInvalidationListener;
@ -51,6 +52,7 @@ use App\Entity\Attachments\LabelAttachment;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
@ -66,7 +68,7 @@ class LabelProfile extends AttachmentContainingDBElement
* @var Collection<int, LabelAttachment>
*/
#[ORM\OneToMany(mappedBy: 'element', targetEntity: LabelAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: LabelAttachment::class)]
@ -78,6 +80,7 @@ class LabelProfile extends AttachmentContainingDBElement
*/
#[Assert\Valid]
#[ORM\Embedded(class: 'LabelOptions')]
#[Groups(["extended", "full", "import"])]
protected LabelOptions $options;
/**
@ -90,6 +93,7 @@ class LabelProfile extends AttachmentContainingDBElement
* @var bool determines, if this label profile should be shown in the dropdown quick menu
*/
#[ORM\Column(type: Types::BOOLEAN)]
#[Groups(["extended", "full", "import"])]
protected bool $show_in_dropdown = true;
public function __construct()

View file

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
@ -17,7 +20,6 @@
* 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\Entity\LabelSystem;
use App\Entity\Parts\Part;
@ -42,4 +44,4 @@ enum LabelSupportedElement: string
self::STORELOCATION => StorageLocation::class,
};
}
}
}

View file

@ -25,7 +25,7 @@ namespace App\Entity\LogSystem;
use Doctrine\DBAL\Types\Types;
use App\Entity\Base\AbstractDBElement;
use App\Entity\UserSystem\User;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use App\Repository\LogEntryRepository;
@ -55,10 +55,11 @@ abstract class AbstractLogEntry extends AbstractDBElement
#[ORM\Column(type: Types::STRING)]
protected string $username = '';
/** @var \DateTimeInterface The datetime the event associated with this log entry has occured
/**
* @var \DateTimeImmutable The datetime the event associated with this log entry has occured
*/
#[ORM\Column(name: 'datetime', type: Types::DATETIME_MUTABLE)]
protected \DateTimeInterface $timestamp;
#[ORM\Column(name: 'datetime', type: Types::DATETIME_IMMUTABLE)]
protected \DateTimeImmutable $timestamp;
/**
* @var LogLevel The priority level of the associated level. 0 is highest, 7 lowest
@ -89,7 +90,7 @@ abstract class AbstractLogEntry extends AbstractDBElement
public function __construct()
{
$this->timestamp = new DateTime();
$this->timestamp = new \DateTimeImmutable();
}
/**
@ -164,7 +165,7 @@ abstract class AbstractLogEntry extends AbstractDBElement
/**
* Returns the timestamp when the event that caused this log entry happened.
*/
public function getTimestamp(): \DateTimeInterface
public function getTimestamp(): \DateTimeImmutable
{
return $this->timestamp;
}
@ -174,7 +175,7 @@ abstract class AbstractLogEntry extends AbstractDBElement
*
* @return $this
*/
public function setTimestamp(\DateTimeInterface $timestamp): self
public function setTimestamp(\DateTimeImmutable $timestamp): self
{
$this->timestamp = $timestamp;

View file

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
@ -17,7 +20,6 @@
* 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\Entity\LogSystem;
use Psr\Log\LogLevel as PSRLogLevel;

View file

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
@ -17,7 +20,6 @@
* 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\Entity\LogSystem;
use App\Entity\Attachments\Attachment;
@ -120,7 +122,7 @@ enum LogTargetType: int
}
}
$elementClass = is_object($element) ? get_class($element) : $element;
$elementClass = is_object($element) ? $element::class : $element;
//If no matching type was found, throw an exception
throw new \InvalidArgumentException("The given class $elementClass is not a valid log target type.");
}

View file

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
@ -17,7 +20,6 @@
* 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\Entity\LogSystem;
use App\Entity\Contracts\LogWithEventUndoInterface;
@ -48,4 +50,4 @@ trait LogWithEventUndoTrait
$mode_int = $this->extra['um'] ?? 1;
return EventUndoMode::fromExtraInt($mode_int);
}
}
}

View file

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
@ -17,7 +20,6 @@
* 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\Entity\LogSystem;
enum PartStockChangeType: string
@ -53,4 +55,4 @@ enum PartStockChangeType: string
default => throw new \InvalidArgumentException("Invalid short type: $value"),
};
}
}
}

View file

@ -52,8 +52,6 @@ class PartStockChangedLogEntry extends AbstractLogEntry
$this->level = LogLevel::INFO;
$this->setTargetElement($lot);
$this->typeString = 'part_stock_changed';
$this->extra = array_merge($this->extra, [
't' => $type->toExtraShortType(),
'o' => $old_stock,

View file

@ -38,15 +38,15 @@ use League\OAuth2\Client\Token\AccessTokenInterface;
class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface
{
/** @var string|null The short-term usable OAuth2 token */
#[ORM\Column(type: 'text', nullable: true)]
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $token = null;
/** @var \DateTimeInterface The date when the token expires */
/** @var \DateTimeImmutable|null The date when the token expires */
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeInterface $expires_at = null;
private ?\DateTimeImmutable $expires_at = null;
/** @var string|null The refresh token for the OAuth2 auth */
#[ORM\Column(type: 'text', nullable: true)]
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $refresh_token = null;
/**
@ -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, \DateTimeInterface $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) {
@ -82,7 +82,7 @@ class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface
);
}
private static function unixTimestampToDatetime(int $timestamp): \DateTimeInterface
private static function unixTimestampToDatetime(int $timestamp): \DateTimeImmutable
{
return \DateTimeImmutable::createFromFormat('U', (string)$timestamp);
}
@ -92,7 +92,7 @@ class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface
return $this->token;
}
public function getExpirationDate(): ?\DateTimeInterface
public function getExpirationDate(): ?\DateTimeImmutable
{
return $this->expires_at;
}

View file

@ -116,7 +116,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
* @var string The mathematical symbol for this specification. Can be rendered pretty later. Should be short
*/
#[Assert\Length(max: 20)]
#[Groups(['full', 'parameter:read', 'parameter:write'])]
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
#[ORM\Column(type: Types::STRING)]
protected string $symbol = '';
@ -126,7 +126,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
#[Assert\Type(['float', null])]
#[Assert\LessThanOrEqual(propertyPath: 'value_typical', message: 'parameters.validator.min_lesser_typical')]
#[Assert\LessThan(propertyPath: 'value_max', message: 'parameters.validator.min_lesser_max')]
#[Groups(['full', 'parameter:read', 'parameter:write'])]
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
#[ORM\Column(type: Types::FLOAT, nullable: true)]
protected ?float $value_min = null;
@ -134,7 +134,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
* @var float|null the typical value of this property
*/
#[Assert\Type([null, 'float'])]
#[Groups(['full', 'parameter:read', 'parameter:write'])]
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
#[ORM\Column(type: Types::FLOAT, nullable: true)]
protected ?float $value_typical = null;
@ -143,14 +143,14 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
*/
#[Assert\Type(['float', null])]
#[Assert\GreaterThanOrEqual(propertyPath: 'value_typical', message: 'parameters.validator.max_greater_typical')]
#[Groups(['full', 'parameter:read', 'parameter:write'])]
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
#[ORM\Column(type: Types::FLOAT, nullable: true)]
protected ?float $value_max = null;
/**
* @var string The unit in which the value values are given (e.g. V)
*/
#[Groups(['full', 'parameter:read', 'parameter:write'])]
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
#[ORM\Column(type: Types::STRING)]
#[Assert\Length(max: 50)]
protected string $unit = '';
@ -158,7 +158,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
/**
* @var string a text value for the given property
*/
#[Groups(['full', 'parameter:read', 'parameter:write'])]
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
#[ORM\Column(type: Types::STRING)]
#[Assert\Length(max: 255)]
protected string $value_text = '';
@ -166,7 +166,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
/**
* @var string the group this parameter belongs to
*/
#[Groups(['full', 'parameter:read', 'parameter:write'])]
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
#[ORM\Column(name: 'param_group', type: Types::STRING)]
#[Assert\Length(max: 255)]
protected string $group = '';

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
@ -91,7 +92,7 @@ use Symfony\Component\Validator\Constraints as Assert;
class Category extends AbstractPartsContainingDBElement
{
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $children;
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
@ -165,7 +166,7 @@ class Category extends AbstractPartsContainingDBElement
#[Assert\Valid]
#[Groups(['full', 'category:read', 'category:write'])]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: CategoryAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: CategoryAttachment::class)]
@ -178,13 +179,13 @@ class Category extends AbstractPartsContainingDBElement
#[Assert\Valid]
#[Groups(['full', 'category:read', 'category:write'])]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: CategoryParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
protected Collection $parameters;
#[Groups(['category:read'])]
protected ?\DateTimeInterface $addedDate = null;
protected ?\DateTimeImmutable $addedDate = null;
#[Groups(['category:read'])]
protected ?\DateTimeInterface $lastModified = null;
protected ?\DateTimeImmutable $lastModified = null;
#[Assert\Valid]
#[ORM\Embedded(class: EDACategoryInfo::class)]

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
@ -96,7 +97,7 @@ class Footprint extends AbstractPartsContainingDBElement
protected ?AbstractStructuralDBElement $parent = null;
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $children;
#[Groups(['footprint:read', 'footprint:write'])]
@ -107,7 +108,7 @@ class Footprint extends AbstractPartsContainingDBElement
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: FootprintAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
#[Groups(['footprint:read', 'footprint:write'])]
protected Collection $attachments;
@ -128,14 +129,14 @@ class Footprint extends AbstractPartsContainingDBElement
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: FootprintParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
#[Groups(['footprint:read', 'footprint:write'])]
protected Collection $parameters;
#[Groups(['footprint:read'])]
protected ?\DateTimeInterface $addedDate = null;
protected ?\DateTimeImmutable $addedDate = null;
#[Groups(['footprint:read'])]
protected ?\DateTimeInterface $lastModified = null;
protected ?\DateTimeImmutable $lastModified = null;
#[Assert\Valid]
#[ORM\Embedded(class: EDAFootprintInfo::class)]

View file

@ -31,31 +31,32 @@ use Symfony\Component\Serializer\Annotation\Groups;
/**
* This class represents a reference to a info provider inside a part.
* @see \App\Tests\Entity\Parts\InfoProviderReferenceTest
*/
#[Embeddable]
class InfoProviderReference
{
/** @var string|null The key referencing the provider used to get this part, or null if it was not provided by a data provider */
#[Column(type: 'string', nullable: true)]
#[Groups(['provider_reference:read'])]
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['provider_reference:read', 'full'])]
private ?string $provider_key = null;
/** @var string|null The id of this part inside the provider system or null if the part was not provided by a data provider */
#[Column(type: 'string', nullable: true)]
#[Groups(['provider_reference:read'])]
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['provider_reference:read', 'full'])]
private ?string $provider_id = null;
/**
* @var string|null The url of this part inside the provider system or null if this info is not existing
*/
#[Column(type: 'string', nullable: true)]
#[Groups(['provider_reference:read'])]
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['provider_reference:read', 'full'])]
private ?string $provider_url = null;
#[Column(type: Types::DATETIME_MUTABLE, nullable: true, options: ['default' => null])]
#[Groups(['provider_reference:read'])]
private ?\DateTimeInterface $last_updated = null;
#[Column(type: Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])]
#[Groups(['provider_reference:read', 'full'])]
private ?\DateTimeImmutable $last_updated = null;
/**
* Constructing is forbidden from outside.
@ -94,9 +95,8 @@ class InfoProviderReference
/**
* Gets the time, when the part was last time updated by the provider.
* @return \DateTimeInterface|null
*/
public function getLastUpdated(): ?\DateTimeInterface
public function getLastUpdated(): ?\DateTimeImmutable
{
return $this->last_updated;
}

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
@ -95,7 +96,7 @@ class Manufacturer extends AbstractCompany
protected ?AbstractStructuralDBElement $parent = null;
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $children;
/**
@ -103,7 +104,7 @@ class Manufacturer extends AbstractCompany
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: ManufacturerAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
#[Groups(['manufacturer:read', 'manufacturer:write'])]
#[ApiProperty(readableLink: false, writableLink: true)]
protected Collection $attachments;
@ -118,7 +119,7 @@ class Manufacturer extends AbstractCompany
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: ManufacturerParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
#[Groups(['manufacturer:read', 'manufacturer:write'])]
#[ApiProperty(readableLink: false, writableLink: true)]
protected Collection $parameters;

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
@ -98,7 +99,7 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
* or m (for meters).
*/
#[Assert\Length(max: 10)]
#[Groups(['extended', 'full', 'import', 'measurement_unit:read', 'measurement_unit:write'])]
#[Groups(['simple', 'extended', 'full', 'import', 'measurement_unit:read', 'measurement_unit:write'])]
#[ORM\Column(name: 'unit', type: Types::STRING, nullable: true)]
protected ?string $unit = null;
@ -109,7 +110,7 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
* @var bool Determines if the amount value associated with this unit should be treated as integer.
* Set to false, to measure continuous sizes likes masses or lengths.
*/
#[Groups(['extended', 'full', 'import', 'measurement_unit:read', 'measurement_unit:write'])]
#[Groups(['simple', 'extended', 'full', 'import', 'measurement_unit:read', 'measurement_unit:write'])]
#[ORM\Column(name: 'is_integer', type: Types::BOOLEAN)]
protected bool $is_integer = false;
@ -118,12 +119,12 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
* Useful for sizes like meters. For this the unit must be set
*/
#[Assert\Expression('this.isUseSIPrefix() == false or this.getUnit() != null', message: 'validator.measurement_unit.use_si_prefix_needs_unit')]
#[Groups(['full', 'import', 'measurement_unit:read', 'measurement_unit:write'])]
#[Groups(['simple', 'full', 'import', 'measurement_unit:read', 'measurement_unit:write'])]
#[ORM\Column(name: 'use_si_prefix', type: Types::BOOLEAN)]
protected bool $use_si_prefix = false;
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class, cascade: ['persist'])]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $children;
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
@ -137,7 +138,7 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: MeasurementUnitAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
#[Groups(['measurement_unit:read', 'measurement_unit:write'])]
protected Collection $attachments;
@ -150,14 +151,14 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: MeasurementUnitParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
#[Groups(['measurement_unit:read', 'measurement_unit:write'])]
protected Collection $parameters;
#[Groups(['measurement_unit:read'])]
protected ?\DateTimeInterface $addedDate = null;
protected ?\DateTimeImmutable $addedDate = null;
#[Groups(['measurement_unit:read'])]
protected ?\DateTimeInterface $lastModified = null;
protected ?\DateTimeImmutable $lastModified = null;
/**

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
@ -119,7 +120,7 @@ class Part extends AttachmentContainingDBElement
#[Assert\Valid]
#[Groups(['full', 'part:read', 'part:write'])]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: PartParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
#[UniqueObjectCollection(fields: ['name', 'group', 'element'])]
protected Collection $parameters;
@ -140,7 +141,7 @@ class Part extends AttachmentContainingDBElement
#[Assert\Valid]
#[Groups(['full', 'part:read', 'part:write'])]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: PartAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $attachments;
/**
@ -153,9 +154,9 @@ class Part extends AttachmentContainingDBElement
protected ?Attachment $master_picture_attachment = null;
#[Groups(['part:read'])]
protected ?\DateTimeInterface $addedDate = null;
protected ?\DateTimeImmutable $addedDate = null;
#[Groups(['part:read'])]
protected ?\DateTimeInterface $lastModified = null;
protected ?\DateTimeImmutable $lastModified = null;
public function __construct()

View file

@ -49,6 +49,7 @@ use Symfony\Component\Validator\Constraints\Length;
/**
* This entity describes a part association, which is a semantic connection between two parts.
* For example, a part association can be used to describe that a part is a replacement for another part.
* @see \App\Tests\Entity\Parts\PartAssociationTest
*/
#[ORM\Entity(repositoryClass: DBElementRepository::class)]
#[ORM\HasLifecycleCallbacks]

View file

@ -105,13 +105,13 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
protected string $comment = '';
/**
* @var \DateTimeInterface|null Set a time until when the lot must be used.
* @var \DateTimeImmutable|null Set a time until when the lot must be used.
* Set to null, if the lot can be used indefinitely.
*/
#[Groups(['extended', 'full', 'import', 'part_lot:read', 'part_lot:write'])]
#[ORM\Column(name: 'expiration_date', type: Types::DATETIME_MUTABLE, nullable: true)]
#[ORM\Column(name: 'expiration_date', type: Types::DATETIME_IMMUTABLE, nullable: true)]
#[Year2038BugWorkaround]
protected ?\DateTimeInterface $expiration_date = null;
protected ?\DateTimeImmutable $expiration_date = null;
/**
* @var StorageLocation|null The storelocation of this lot
@ -194,7 +194,7 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
}
//Check if the expiration date is bigger then current time
return $this->expiration_date < new DateTime('now');
return $this->expiration_date < new \DateTimeImmutable('now');
}
/**
@ -236,7 +236,7 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
/**
* Gets the expiration date for the part lot. Returns null, if no expiration date was set.
*/
public function getExpirationDate(): ?\DateTimeInterface
public function getExpirationDate(): ?\DateTimeImmutable
{
return $this->expiration_date;
}
@ -246,7 +246,7 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
*
*
*/
public function setExpirationDate(?\DateTimeInterface $expiration_date): self
public function setExpirationDate(?\DateTimeImmutable $expiration_date): self
{
$this->expiration_date = $expiration_date;

View file

@ -38,7 +38,7 @@ trait AssociationTrait
#[Valid]
#[ORM\OneToMany(mappedBy: 'owner', targetEntity: PartAssociation::class,
cascade: ['persist', 'remove'], orphanRemoval: true)]
#[Groups(['part:read', 'part:write'])]
#[Groups(['part:read', 'part:write', 'full'])]
protected Collection $associated_parts_as_owner;
/**

View file

@ -32,7 +32,7 @@ trait EDATrait
{
#[Valid]
#[Embedded(class: EDAPartInfo::class)]
#[Groups(['full', 'part:read', 'part:write'])]
#[Groups(['full', 'part:read', 'part:write', 'import'])]
protected EDAPartInfo $eda_info;
public function getEdaInfo(): EDAPartInfo

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts\PartTraits;
use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\Types\Types;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\PartLot;
@ -42,7 +43,7 @@ trait InstockTrait
#[Assert\Valid]
#[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])]
#[ORM\OneToMany(mappedBy: 'part', targetEntity: PartLot::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['amount' => 'DESC'])]
#[ORM\OrderBy(['amount' => Criteria::DESC])]
protected Collection $partLots;
/**

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts\PartTraits;
use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\Types\Types;
use App\Entity\PriceInformations\Orderdetail;
use Symfony\Component\Serializer\Annotation\Groups;
@ -41,7 +42,7 @@ trait OrderTrait
#[Assert\Valid]
#[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])]
#[ORM\OneToMany(mappedBy: 'part', targetEntity: Orderdetail::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['supplierpartnr' => 'ASC'])]
#[ORM\OrderBy(['supplierpartnr' => Criteria::ASC])]
protected Collection $orderdetails;
/**

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
@ -90,7 +91,7 @@ use Symfony\Component\Validator\Constraints as Assert;
class StorageLocation extends AbstractPartsContainingDBElement
{
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $children;
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
@ -114,7 +115,7 @@ class StorageLocation extends AbstractPartsContainingDBElement
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: StorageLocationParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
#[Groups(['location:read', 'location:write'])]
protected Collection $parameters;
@ -169,9 +170,9 @@ class StorageLocation extends AbstractPartsContainingDBElement
protected ?Attachment $master_picture_attachment = null;
#[Groups(['location:read'])]
protected ?\DateTimeInterface $addedDate = null;
protected ?\DateTimeImmutable $addedDate = null;
#[Groups(['location:read'])]
protected ?\DateTimeInterface $lastModified = null;
protected ?\DateTimeImmutable $lastModified = null;
/********************************************************************************

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
@ -92,7 +93,7 @@ use Symfony\Component\Validator\Constraints as Assert;
class Supplier extends AbstractCompany
{
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $children;
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
@ -129,7 +130,7 @@ class Supplier extends AbstractCompany
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: SupplierAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
#[Groups(['supplier:read', 'supplier:write'])]
#[ApiProperty(readableLink: false, writableLink: true)]
protected Collection $attachments;
@ -144,7 +145,7 @@ class Supplier extends AbstractCompany
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: SupplierParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
#[Groups(['supplier:read', 'supplier:write'])]
#[ApiProperty(readableLink: false, writableLink: true)]
protected Collection $parameters;

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\PriceInformations;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
@ -101,7 +102,7 @@ class Currency extends AbstractStructuralDBElement
*/
#[ORM\Column(type: 'big_decimal', precision: 11, scale: 5, nullable: true)]
#[BigDecimalPositive]
#[Groups(['currency:read', 'currency:write'])]
#[Groups(['currency:read', 'currency:write', 'simple', 'extended', 'full', 'import'])]
#[ApiProperty(readableLink: false, writableLink: false)]
protected ?BigDecimal $exchange_rate = null;
@ -113,12 +114,12 @@ class Currency extends AbstractStructuralDBElement
*/
#[Assert\Currency]
#[Assert\NotBlank]
#[Groups(['extended', 'full', 'import', 'currency:read', 'currency:write'])]
#[Groups(['simple', 'extended', 'full', 'import', 'currency:read', 'currency:write'])]
#[ORM\Column(type: Types::STRING)]
protected string $iso_code = "";
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class, cascade: ['persist'])]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $children;
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
@ -132,7 +133,7 @@ class Currency extends AbstractStructuralDBElement
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: CurrencyAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
#[Groups(['currency:read', 'currency:write'])]
protected Collection $attachments;
@ -145,7 +146,7 @@ class Currency extends AbstractStructuralDBElement
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: CurrencyParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
#[Groups(['currency:read', 'currency:write'])]
protected Collection $parameters;
@ -155,9 +156,9 @@ class Currency extends AbstractStructuralDBElement
protected Collection $pricedetails;
#[Groups(['currency:read'])]
protected ?\DateTimeInterface $addedDate = null;
protected ?\DateTimeImmutable $addedDate = null;
#[Groups(['currency:read'])]
protected ?\DateTimeInterface $lastModified = null;
protected ?\DateTimeImmutable $lastModified = null;
public function __construct()

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Entity\PriceInformations;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
@ -45,7 +46,7 @@ use App\Entity\Contracts\NamedElementInterface;
use App\Entity\Contracts\TimeStampableInterface;
use App\Entity\Parts\Part;
use App\Entity\Parts\Supplier;
use DateTime;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
@ -96,10 +97,13 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N
{
use TimestampTrait;
/**
* @var Collection<int, Pricedetail>
*/
#[Assert\Valid]
#[Groups(['extended', 'full', 'import', 'orderdetail:read', 'orderdetail:write'])]
#[ORM\OneToMany(mappedBy: 'orderdetail', targetEntity: Pricedetail::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['min_discount_quantity' => 'ASC'])]
#[ORM\OrderBy(['min_discount_quantity' => Criteria::ASC])]
protected Collection $pricedetails;
/**
@ -169,9 +173,9 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N
#[ORM\PreUpdate]
public function updateTimestamps(): void
{
$this->lastModified = new DateTime('now');
$this->lastModified = new DateTimeImmutable('now');
if (!$this->addedDate instanceof \DateTimeInterface) {
$this->addedDate = new DateTime('now');
$this->addedDate = new DateTimeImmutable('now');
}
if ($this->part instanceof Part) {

View file

@ -38,7 +38,7 @@ use App\Validator\Constraints\BigDecimal\BigDecimalPositive;
use App\Validator\Constraints\Selectable;
use Brick\Math\BigDecimal;
use Brick\Math\RoundingMode;
use DateTime;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
@ -141,9 +141,9 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface
#[ORM\PreUpdate]
public function updateTimestamps(): void
{
$this->lastModified = new DateTime('now');
$this->lastModified = new DateTimeImmutable('now');
if (!$this->addedDate instanceof \DateTimeInterface) {
$this->addedDate = new DateTime('now');
$this->addedDate = new DateTimeImmutable('now');
}
if ($this->orderdetail instanceof Orderdetail) {

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\ProjectSystem;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty;
@ -88,7 +89,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
class Project extends AbstractStructuralDBElement
{
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $children;
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
@ -100,8 +101,11 @@ class Project extends AbstractStructuralDBElement
#[Groups(['project:read', 'project:write'])]
protected string $comment = '';
/**
* @var Collection<int, ProjectBOMEntry>
*/
#[Assert\Valid]
#[Groups(['extended', 'full'])]
#[Groups(['extended', 'full', 'import'])]
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part'])]
#[UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name'])]
@ -114,7 +118,7 @@ class Project extends AbstractStructuralDBElement
* @var string|null The current status of the project
*/
#[Assert\Choice(['draft', 'planning', 'in_production', 'finished', 'archived'])]
#[Groups(['extended', 'full', 'project:read', 'project:write'])]
#[Groups(['extended', 'full', 'project:read', 'project:write', 'import'])]
#[ORM\Column(type: Types::STRING, length: 64, nullable: true)]
protected ?string $status = null;
@ -137,7 +141,7 @@ class Project extends AbstractStructuralDBElement
* @var Collection<int, ProjectAttachment>
*/
#[ORM\OneToMany(mappedBy: 'element', targetEntity: ProjectAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
#[Groups(['project:read', 'project:write'])]
protected Collection $attachments;
@ -149,14 +153,14 @@ class Project extends AbstractStructuralDBElement
/** @var Collection<int, ProjectParameter>
*/
#[ORM\OneToMany(mappedBy: 'element', targetEntity: ProjectParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
#[Groups(['project:read', 'project:write'])]
protected Collection $parameters;
#[Groups(['project:read'])]
protected ?\DateTimeInterface $addedDate = null;
protected ?\DateTimeImmutable $addedDate = null;
#[Groups(['project:read'])]
protected ?\DateTimeInterface $lastModified = null;
protected ?\DateTimeImmutable $lastModified = null;
/********************************************************************************

View file

@ -90,14 +90,14 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte
#[Assert\Positive]
#[ORM\Column(name: 'quantity', type: Types::FLOAT)]
#[Groups(['bom_entry:read', 'bom_entry:write'])]
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])]
protected float $quantity = 1.0;
/**
* @var string A comma separated list of the names, where this parts should be placed
*/
#[ORM\Column(name: 'mountnames', type: Types::TEXT)]
#[Groups(['bom_entry:read', 'bom_entry:write'])]
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])]
protected string $mountnames = '';
/**
@ -105,14 +105,14 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte
*/
#[Assert\Expression('this.getPart() !== null or this.getName() !== null', message: 'validator.project.bom_entry.name_or_part_needed')]
#[ORM\Column(type: Types::STRING, nullable: true)]
#[Groups(['bom_entry:read', 'bom_entry:write'])]
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])]
protected ?string $name = null;
/**
* @var string An optional comment for this BOM entry
*/
#[ORM\Column(type: Types::TEXT)]
#[Groups(['bom_entry:read', 'bom_entry:write'])]
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full'])]
protected string $comment = '';
/**
@ -120,7 +120,7 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte
*/
#[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'bom_entries')]
#[ORM\JoinColumn(name: 'id_device')]
#[Groups(['bom_entry:read', 'bom_entry:write'])]
#[Groups(['bom_entry:read', 'bom_entry:write', ])]
protected ?Project $project = null;
/**
@ -128,7 +128,7 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte
*/
#[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'project_bom_entries')]
#[ORM\JoinColumn(name: 'id_part')]
#[Groups(['bom_entry:read', 'bom_entry:write'])]
#[Groups(['bom_entry:read', 'bom_entry:write', 'full'])]
protected ?Part $part = null;
/**
@ -136,7 +136,7 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte
*/
#[Assert\AtLeastOneOf([new BigDecimalPositive(), new Assert\IsNull()])]
#[ORM\Column(type: 'big_decimal', precision: 11, scale: 5, nullable: true)]
#[Groups(['bom_entry:read', 'bom_entry:write'])]
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full'])]
protected ?BigDecimal $price = null;
/**

View file

@ -75,10 +75,10 @@ class ApiToken implements TimeStampableInterface
#[Groups('token:read')]
private ?User $user = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
#[Groups('token:read')]
#[Year2038BugWorkaround]
private ?\DateTimeInterface $valid_until;
private ?\DateTimeImmutable $valid_until;
#[ORM\Column(length: 68, unique: true)]
private string $token;
@ -87,9 +87,9 @@ class ApiToken implements TimeStampableInterface
#[Groups('token:read')]
private ApiTokenLevel $level = ApiTokenLevel::READ_ONLY;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
#[Groups('token:read')]
private ?\DateTimeInterface $last_time_used = null;
private ?\DateTimeImmutable $last_time_used = null;
public function __construct(ApiTokenType $tokenType = ApiTokenType::PERSONAL_ACCESS_TOKEN)
{
@ -97,7 +97,7 @@ class ApiToken implements TimeStampableInterface
$this->token = $tokenType->getTokenPrefix() . bin2hex(random_bytes(32));
//By default, tokens are valid for 1 year.
$this->valid_until = new \DateTime('+1 year');
$this->valid_until = new \DateTimeImmutable('+1 year');
}
public function getTokenType(): ApiTokenType
@ -116,7 +116,7 @@ class ApiToken implements TimeStampableInterface
return $this;
}
public function getValidUntil(): ?\DateTimeInterface
public function getValidUntil(): ?\DateTimeImmutable
{
return $this->valid_until;
}
@ -127,10 +127,10 @@ class ApiToken implements TimeStampableInterface
*/
public function isValid(): bool
{
return $this->valid_until === null || $this->valid_until > new \DateTime();
return $this->valid_until === null || $this->valid_until > new \DateTimeImmutable();
}
public function setValidUntil(?\DateTimeInterface $valid_until): ApiToken
public function setValidUntil(?\DateTimeImmutable $valid_until): ApiToken
{
$this->valid_until = $valid_until;
return $this;
@ -159,19 +159,17 @@ class ApiToken implements TimeStampableInterface
/**
* Gets the last time the token was used to authenticate or null if it was never used.
* @return \DateTimeInterface|null
*/
public function getLastTimeUsed(): ?\DateTimeInterface
public function getLastTimeUsed(): ?\DateTimeImmutable
{
return $this->last_time_used;
}
/**
* Sets the last time the token was used to authenticate.
* @param \DateTimeInterface|null $last_time_used
* @return ApiToken
*/
public function setLastTimeUsed(?\DateTimeInterface $last_time_used): ApiToken
public function setLastTimeUsed(?\DateTimeImmutable $last_time_used): ApiToken
{
$this->last_time_used = $last_time_used;
return $this;

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\UserSystem;
use Doctrine\Common\Collections\Criteria;
use App\Entity\Attachments\Attachment;
use App\Validator\Constraints\NoLockout;
use Doctrine\DBAL\Types\Types;
@ -49,7 +50,7 @@ use Symfony\Component\Validator\Constraints as Assert;
class Group extends AbstractStructuralDBElement implements HasPermissionsInterface
{
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $children;
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
@ -74,7 +75,7 @@ class Group extends AbstractStructuralDBElement implements HasPermissionsInterfa
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: GroupAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: GroupAttachment::class)]
@ -91,7 +92,7 @@ class Group extends AbstractStructuralDBElement implements HasPermissionsInterfa
*/
#[Assert\Valid]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: GroupParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
protected Collection $parameters;
public function __construct()

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\UserSystem;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
@ -116,10 +117,10 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
protected ?int $id = null;
#[Groups(['user:read'])]
protected ?\DateTimeInterface $lastModified = null;
protected ?\DateTimeImmutable $lastModified = null;
#[Groups(['user:read'])]
protected ?\DateTimeInterface $addedDate = null;
protected ?\DateTimeImmutable $addedDate = null;
/**
* @var bool Determines if the user is disabled (user can not log in)
@ -143,9 +144,11 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
protected ?string $pw_reset_token = null;
#[ORM\Column(name: 'config_instock_comment_a', type: Types::TEXT)]
#[Groups(['extended', 'full', 'import'])]
protected string $instock_comment_a = '';
#[ORM\Column(name: 'config_instock_comment_w', type: Types::TEXT)]
#[Groups(['extended', 'full', 'import'])]
protected string $instock_comment_w = '';
/**
@ -267,7 +270,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
* @var Collection<int, UserAttachment>
*/
#[ORM\OneToMany(mappedBy: 'element', targetEntity: UserAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
#[ORM\OrderBy(['name' => Criteria::ASC])]
#[Groups(['user:read', 'user:write'])]
protected Collection $attachments;
@ -276,11 +279,11 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
#[Groups(['user:read', 'user:write'])]
protected ?Attachment $master_picture_attachment = null;
/** @var \DateTimeInterface|null The time when the backup codes were generated
/** @var \DateTimeImmutable|null The time when the backup codes were generated
*/
#[Groups(['full'])]
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
protected ?\DateTimeInterface $backupCodesGenerationDate = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
protected ?\DateTimeImmutable $backupCodesGenerationDate = null;
/** @var Collection<int, LegacyU2FKeyInterface>
*/
@ -317,10 +320,10 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
protected ?PermissionData $permissions = null;
/**
* @var \DateTimeInterface|null the time until the password reset token is valid
* @var \DateTimeImmutable|null the time until the password reset token is valid
*/
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
protected ?\DateTimeInterface $pw_reset_expires = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
protected ?\DateTimeImmutable $pw_reset_expires = null;
/**
* @var bool True if the user was created by a SAML provider (and therefore cannot change its password)
@ -527,7 +530,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
/**
* Gets the datetime when the password reset token expires.
*/
public function getPwResetExpires(): \DateTimeInterface|null
public function getPwResetExpires(): \DateTimeImmutable|null
{
return $this->pw_reset_expires;
}
@ -535,7 +538,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
/**
* Sets the datetime when the password reset token expires.
*/
public function setPwResetExpires(\DateTimeInterface $pw_reset_expires): self
public function setPwResetExpires(\DateTimeImmutable $pw_reset_expires): self
{
$this->pw_reset_expires = $pw_reset_expires;
@ -896,7 +899,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
public function setBackupCodes(array $codes): self
{
$this->backupCodes = $codes;
$this->backupCodesGenerationDate = $codes === [] ? null : new DateTime();
$this->backupCodesGenerationDate = $codes === [] ? null : new \DateTimeImmutable();
return $this;
}
@ -904,7 +907,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
/**
* Return the date when the backup codes were generated.
*/
public function getBackupCodesGenerationDate(): ?\DateTimeInterface
public function getBackupCodesGenerationDate(): ?\DateTimeImmutable
{
return $this->backupCodesGenerationDate;
}

View file

@ -51,7 +51,7 @@ class WebauthnKey extends BasePublicKeyCredentialSource implements TimeStampable
protected ?User $user = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
protected ?\DateTimeInterface $last_time_used = null;
protected ?\DateTimeImmutable $last_time_used = null;
public function getName(): string
{
@ -82,9 +82,8 @@ class WebauthnKey extends BasePublicKeyCredentialSource implements TimeStampable
/**
* Retrieve the last time when the key was used.
* @return \DateTimeInterface|null
*/
public function getLastTimeUsed(): ?\DateTimeInterface
public function getLastTimeUsed(): ?\DateTimeImmutable
{
return $this->last_time_used;
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 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\EventListener;
use App\Doctrine\Functions\Natsort;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
/**
* This is a workaround to the fact that we can not inject parameters into doctrine custom functions.
* Therefore we use this event listener to call the static function on the custom function, to inject the value, before
* any NATSORT function is called.
*/
#[AsEventListener]
class AllowSlowNaturalSortListener
{
public function __construct(
#[Autowire(param: 'partdb.db.emulate_natural_sort')]
private readonly bool $allowNaturalSort)
{
}
public function __invoke(RequestEvent $event)
{
Natsort::allowSlowNaturalSort($this->allowNaturalSort);
}
}

View file

@ -20,7 +20,7 @@
declare(strict_types=1);
namespace App\EventSubscriber\LogSystem;
namespace App\EventListener\LogSystem;
use App\Entity\Attachments\Attachment;
use App\Entity\Base\AbstractDBElement;
@ -38,6 +38,7 @@ use App\Entity\UserSystem\User;
use App\Services\LogSystem\EventCommentHelper;
use App\Services\LogSystem\EventLogger;
use App\Services\LogSystem\EventUndoHelper;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use App\Settings\SystemSettings\HistorySettings;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
@ -51,7 +52,10 @@ use Symfony\Component\Serializer\SerializerInterface;
/**
* This event subscriber writes to the event log when entities are changed, removed, created.
*/
class EventLoggerSubscriber implements EventSubscriber
#[AsDoctrineListener(event: Events::onFlush)]
#[AsDoctrineListener(event: Events::postPersist)]
#[AsDoctrineListener(event: Events::postFlush)]
class EventLoggerListener
{
/**
* @var array The given fields will not be saved, because they contain sensitive information
@ -189,15 +193,6 @@ class EventLoggerSubscriber implements EventSubscriber
return true;
}
public function getSubscribedEvents(): array
{
return[
Events::onFlush,
Events::postPersist,
Events::postFlush,
];
}
protected function logElementDeleted(AbstractDBElement $entity, EntityManagerInterface $em): void
{
$log = new ElementDeletedLogEntry($entity);

View file

@ -20,11 +20,11 @@
declare(strict_types=1);
namespace App\EventSubscriber\LogSystem;
namespace App\EventListener\LogSystem;
use App\Entity\LogSystem\DatabaseUpdatedLogEntry;
use App\Services\LogSystem\EventLogger;
use Doctrine\Common\EventSubscriber;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Migrations\DependencyFactory;
use Doctrine\Migrations\Event\MigrationsEventArgs;
use Doctrine\Migrations\Events;
@ -32,7 +32,9 @@ use Doctrine\Migrations\Events;
/**
* This subscriber logs databaseMigrations to Event log.
*/
class LogDBMigrationSubscriber implements EventSubscriber
#[AsDoctrineListener(event: Events::onMigrationsMigrated)]
#[AsDoctrineListener(event: Events::onMigrationsMigrating)]
class LogDBMigrationListener
{
protected ?string $old_version = null;
protected ?string $new_version = null;

View file

@ -141,7 +141,7 @@ class AttachmentFormType extends AbstractType
if (!$file instanceof UploadedFile) {
//When no file was uploaded, but a URL was entered, try to determine the attachment name from the URL
if (empty($attachment->getName()) && !empty($attachment->getURL())) {
if ((trim($attachment->getName()) === '') && ($attachment->getURL() !== null && $attachment->getURL() !== '')) {
$name = basename(parse_url($attachment->getURL(), PHP_URL_PATH));
$attachment->setName($name);
}

View file

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
@ -17,7 +20,6 @@
* 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\Form;
use Symfony\Component\Form\AbstractTypeExtension;
@ -51,4 +53,4 @@ class PasswordTypeExtension extends AbstractTypeExtension
$view->vars['password_estimator'] = $options['password_estimator'];
}
}
}

View file

@ -45,9 +45,7 @@ class PermissionsType extends AbstractType
$resolver->setDefaults([
'show_legend' => true,
'show_presets' => false,
'show_dependency_notice' => static function (Options $options) {
return !$options['disabled'];
},
'show_dependency_notice' => static fn(Options $options) => !$options['disabled'],
'constraints' => static function (Options $options) {
if (!$options['disabled']) {
return [new NoLockout()];

View file

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
@ -17,7 +20,6 @@
* 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\Form\ProjectSystem;
use App\Entity\ProjectSystem\Project;
@ -83,4 +85,4 @@ class ProjectAddPartsType extends AbstractType
$resolver->setAllowedTypes('project', ['null', Project::class]);
}
}
}

View file

@ -100,7 +100,7 @@ class StructuralEntityChoiceHelper
public function generateChoiceAttrCurrency(Currency $choice, Options|array $options): array
{
$tmp = $this->generateChoiceAttr($choice, $options);
$symbol = empty($choice->getIsoCode()) ? null : Currencies::getSymbol($choice->getIsoCode());
$symbol = $choice->getIsoCode() === '' ? null : Currencies::getSymbol($choice->getIsoCode());
$tmp['data-short'] = $options['short'] ? $symbol : $choice->getName();
//Show entities that are not added to DB yet separately from other entities

Some files were not shown because too many files have changed in this diff Show more