From 8bb8118d9fb2431942233718f8769e8e28f19b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 17 Jun 2024 22:33:40 +0200 Subject: [PATCH] Use natural sorting for trees and others repository functions --- migrations/Version20240606203053.php | 2 +- src/Doctrine/Functions/Natsort.php | 11 +++++--- .../PartsContainingRepositoryInterface.php | 4 +-- .../AbstractPartsContainingRepository.php | 15 +++++++--- src/Repository/NamedDBElementRepository.php | 12 +++++--- src/Repository/PartRepository.php | 2 +- src/Repository/Parts/CategoryRepository.php | 4 +-- src/Repository/Parts/FootprintRepository.php | 4 +-- .../Parts/ManufacturerRepository.php | 4 +-- .../Parts/MeasurementUnitRepository.php | 4 +-- .../Parts/StorelocationRepository.php | 15 +++------- src/Repository/Parts/SupplierRepository.php | 10 +++---- .../StructuralDBElementRepository.php | 28 +++++++++++++++++-- 13 files changed, 71 insertions(+), 44 deletions(-) diff --git a/migrations/Version20240606203053.php b/migrations/Version20240606203053.php index df94814b..bf9cee96 100644 --- a/migrations/Version20240606203053.php +++ b/migrations/Version20240606203053.php @@ -25,7 +25,7 @@ final class Version20240606203053 extends AbstractMultiPlatformMigration impleme public function postgreSQLUp(Schema $schema): void { //Create a collation for natural sorting - $this->addSql("CREATE COLLATION numeric (provider = icu, locale = 'en@colNumeric=yes');"); + $this->addSql("CREATE COLLATION numeric (provider = icu, locale = 'en-u-kn-true');"); $this->addSql('CREATE TABLE api_tokens (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, valid_until TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, token VARCHAR(68) NOT NULL, level SMALLINT NOT NULL, last_time_used TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, user_id INT DEFAULT NULL, PRIMARY KEY(id))'); $this->addSql('CREATE UNIQUE INDEX UNIQ_2CAD560E5F37A13B ON api_tokens (token)'); diff --git a/src/Doctrine/Functions/Natsort.php b/src/Doctrine/Functions/Natsort.php index 51aeb01b..fd4568f8 100644 --- a/src/Doctrine/Functions/Natsort.php +++ b/src/Doctrine/Functions/Natsort.php @@ -25,6 +25,7 @@ namespace App\Doctrine\Functions; use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver; use Doctrine\DBAL\Platforms\MariaDBPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Node; use Doctrine\ORM\Query\Parser; @@ -33,7 +34,7 @@ use Doctrine\ORM\Query\TokenType; class Natsort extends FunctionNode { - private Node $field; + private ?Node $field = null; public function parse(Parser $parser): void { @@ -47,15 +48,17 @@ class Natsort extends FunctionNode public function getSql(SqlWalker $sqlWalker): string { + assert($this->field !== null, 'Field is not set'); + $platform = $sqlWalker->getConnection()->getDatabasePlatform(); - if ($platform instanceof AbstractPostgreSQLDriver) { + if ($platform instanceof PostgreSQLPlatform) { return $this->field->dispatch($sqlWalker) . ' COLLATE numeric'; } - if ($platform instanceof MariaDBPlatform && $sqlWalker->getConnection()->getServerVersion()) { + /*if ($platform instanceof MariaDBPlatform && $sqlWalker->getConnection()->getServerVersion()) { - } + }*/ //For every other platform, return the field as is return $this->field->dispatch($sqlWalker); diff --git a/src/Entity/Base/PartsContainingRepositoryInterface.php b/src/Entity/Base/PartsContainingRepositoryInterface.php index f852bc35..89e7e5f6 100644 --- a/src/Entity/Base/PartsContainingRepositoryInterface.php +++ b/src/Entity/Base/PartsContainingRepositoryInterface.php @@ -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. diff --git a/src/Repository/AbstractPartsContainingRepository.php b/src/Repository/AbstractPartsContainingRepository.php index e3c7f610..4fd0bbff 100644 --- a/src/Repository/AbstractPartsContainingRepository.php +++ b/src/Repository/AbstractPartsContainingRepository.php @@ -40,11 +40,11 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo * 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[] */ - abstract public function getParts(object $element, array $order_by = ['name' => 'ASC']): array; + abstract public function getParts(object $element, string $nameOrderDirection = "ASC"): array; /** * Gets the count of the parts associated with this element. @@ -113,7 +113,7 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo return $parts; } - protected function getPartsByField(object $element, array $order_by, string $field_name): array + protected function getPartsByField(object $element, string $nameOrderDirection, string $field_name): array { if (!$element instanceof AbstractPartsContainingDBElement) { throw new InvalidArgumentException('$element must be an instance of AbstractPartContainingDBElement!'); @@ -121,7 +121,14 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo $repo = $this->getEntityManager()->getRepository(Part::class); - return $repo->findBy([$field_name => $element], $order_by); + //Build a query builder to get the parts with a custom order by + + $qb = $repo->createQueryBuilder('part') + ->where('part.'.$field_name.' = :element') + ->setParameter('element', $element) + ->orderBy('NATSORT(part.name)', $nameOrderDirection); + + return $qb->getQuery()->getResult(); } protected function getPartsCountByField(object $element, string $field_name): int diff --git a/src/Repository/NamedDBElementRepository.php b/src/Repository/NamedDBElementRepository.php index ae8d4d8f..e62c64e7 100644 --- a/src/Repository/NamedDBElementRepository.php +++ b/src/Repository/NamedDBElementRepository.php @@ -42,7 +42,7 @@ class NamedDBElementRepository extends DBElementRepository { $result = []; - $entities = $this->findBy([], ['name' => 'ASC']); + $entities = $this->getFlatList(); foreach ($entities as $entity) { /** @var AbstractNamedDBElement $entity */ $node = new TreeViewNode($entity->getName(), null, null); @@ -65,13 +65,17 @@ class NamedDBElementRepository extends DBElementRepository } /** - * Returns a flattened list of all nodes. + * Returns a flattened list of all nodes, sorted by name in natural order. * @return AbstractNamedDBElement[] * @phpstan-return array */ public function getFlatList(): array { - //All nodes are sorted by name - return $this->findBy([], ['name' => 'ASC']); + $qb = $this->createQueryBuilder('e'); + $q = $qb->select('e') + ->orderBy('NATSORT(e.name)', 'ASC') + ->getQuery(); + + return $q->getResult(); } } diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index 3ab3ed31..84357b72 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -90,7 +90,7 @@ class PartRepository extends NamedDBElementRepository $qb->setParameter('query', '%'.$query.'%'); $qb->setMaxResults($max_limits); - $qb->orderBy('part.name', 'ASC'); + $qb->orderBy('NATSORT(part.name)', 'ASC'); return $qb->getQuery()->getResult(); } diff --git a/src/Repository/Parts/CategoryRepository.php b/src/Repository/Parts/CategoryRepository.php index 8e270047..9bbb1e37 100644 --- a/src/Repository/Parts/CategoryRepository.php +++ b/src/Repository/Parts/CategoryRepository.php @@ -28,13 +28,13 @@ use InvalidArgumentException; class CategoryRepository extends AbstractPartsContainingRepository { - public function getParts(object $element, array $order_by = ['name' => 'ASC']): array + public function getParts(object $element, string $nameOrderDirection = "ASC"): array { if (!$element instanceof Category) { throw new InvalidArgumentException('$element must be an Category!'); } - return $this->getPartsByField($element, $order_by, 'category'); + return $this->getPartsByField($element, $nameOrderDirection, 'category'); } public function getPartsCount(object $element): int diff --git a/src/Repository/Parts/FootprintRepository.php b/src/Repository/Parts/FootprintRepository.php index 355cb1bb..8934831a 100644 --- a/src/Repository/Parts/FootprintRepository.php +++ b/src/Repository/Parts/FootprintRepository.php @@ -28,13 +28,13 @@ use InvalidArgumentException; class FootprintRepository extends AbstractPartsContainingRepository { - public function getParts(object $element, array $order_by = ['name' => 'ASC']): array + public function getParts(object $element, string $nameOrderDirection = "ASC"): array { if (!$element instanceof Footprint) { throw new InvalidArgumentException('$element must be an Footprint!'); } - return $this->getPartsByField($element, $order_by, 'footprint'); + return $this->getPartsByField($element, $nameOrderDirection, 'footprint'); } public function getPartsCount(object $element): int diff --git a/src/Repository/Parts/ManufacturerRepository.php b/src/Repository/Parts/ManufacturerRepository.php index a47142d4..1a838710 100644 --- a/src/Repository/Parts/ManufacturerRepository.php +++ b/src/Repository/Parts/ManufacturerRepository.php @@ -28,13 +28,13 @@ use InvalidArgumentException; class ManufacturerRepository extends AbstractPartsContainingRepository { - public function getParts(object $element, array $order_by = ['name' => 'ASC']): array + public function getParts(object $element, string $nameOrderDirection = "ASC"): array { if (!$element instanceof Manufacturer) { throw new InvalidArgumentException('$element must be an Manufacturer!'); } - return $this->getPartsByField($element, $order_by, 'manufacturer'); + return $this->getPartsByField($element, $nameOrderDirection, 'manufacturer'); } public function getPartsCount(object $element): int diff --git a/src/Repository/Parts/MeasurementUnitRepository.php b/src/Repository/Parts/MeasurementUnitRepository.php index 1c9b106b..c581f751 100644 --- a/src/Repository/Parts/MeasurementUnitRepository.php +++ b/src/Repository/Parts/MeasurementUnitRepository.php @@ -28,13 +28,13 @@ use InvalidArgumentException; class MeasurementUnitRepository extends AbstractPartsContainingRepository { - public function getParts(object $element, array $order_by = ['name' => 'ASC']): array + public function getParts(object $element, string $nameOrderDirection = "ASC"): array { if (!$element instanceof MeasurementUnit) { throw new InvalidArgumentException('$element must be an MeasurementUnit!'); } - return $this->getPartsByField($element, $order_by, 'partUnit'); + return $this->getPartsByField($element, $nameOrderDirection, 'partUnit'); } public function getPartsCount(object $element): int diff --git a/src/Repository/Parts/StorelocationRepository.php b/src/Repository/Parts/StorelocationRepository.php index 192d4f6d..82317868 100644 --- a/src/Repository/Parts/StorelocationRepository.php +++ b/src/Repository/Parts/StorelocationRepository.php @@ -30,12 +30,7 @@ use InvalidArgumentException; class StorelocationRepository extends AbstractPartsContainingRepository { - /** - * @param object $element - * @param array $order_by - * @return array - */ - public function getParts(object $element, array $order_by = ['name' => 'ASC']): array + public function getParts(object $element, string $nameOrderDirection = "ASC"): array { if (!$element instanceof StorageLocation) { throw new InvalidArgumentException('$element must be an Storelocation!'); @@ -47,11 +42,9 @@ class StorelocationRepository extends AbstractPartsContainingRepository ->from(Part::class, 'part') ->leftJoin('part.partLots', 'lots') ->where('lots.storage_location = ?1') - ->setParameter(1, $element); - - foreach ($order_by as $field => $order) { - $qb->addOrderBy('part.'.$field, $order); - } + ->setParameter(1, $element) + ->orderBy('NATSORT(part.name)', $nameOrderDirection) + ; return $qb->getQuery()->getResult(); } diff --git a/src/Repository/Parts/SupplierRepository.php b/src/Repository/Parts/SupplierRepository.php index 6dc995f1..393ae593 100644 --- a/src/Repository/Parts/SupplierRepository.php +++ b/src/Repository/Parts/SupplierRepository.php @@ -30,7 +30,7 @@ use InvalidArgumentException; class SupplierRepository extends AbstractPartsContainingRepository { - public function getParts(object $element, array $order_by = ['name' => 'ASC']): array + public function getParts(object $element, string $nameOrderDirection = "ASC"): array { if (!$element instanceof Supplier) { throw new InvalidArgumentException('$element must be an Supplier!'); @@ -42,11 +42,9 @@ class SupplierRepository extends AbstractPartsContainingRepository ->from(Part::class, 'part') ->leftJoin('part.orderdetails', 'orderdetail') ->where('orderdetail.supplier = ?1') - ->setParameter(1, $element); - - foreach ($order_by as $field => $order) { - $qb->addOrderBy('part.'.$field, $order); - } + ->setParameter(1, $element) + ->orderBy('NATSORT(part.name)', $nameOrderDirection) + ; return $qb->getQuery()->getResult(); } diff --git a/src/Repository/StructuralDBElementRepository.php b/src/Repository/StructuralDBElementRepository.php index 978cee20..c7548cc9 100644 --- a/src/Repository/StructuralDBElementRepository.php +++ b/src/Repository/StructuralDBElementRepository.php @@ -40,6 +40,28 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit */ private array $new_entity_cache = []; + /** + * Finds all nodes for the given parent node, ordered by name in a natural sort way + * @param AbstractStructuralDBElement|null $parent + * @param string $nameOrdering The ordering of the names. Either ASC or DESC + * @return array + */ + public function findNodesForParent(?AbstractStructuralDBElement $parent, string $nameOrdering = "ASC"): array + { + $qb = $this->createQueryBuilder('e'); + $qb->select('e') + ->orderBy('NATSORT(e.name)', $nameOrdering); + + if ($parent) { + $qb->where('e.parent = :parent') + ->setParameter('parent', $parent); + } else { + $qb->where('e.parent IS NULL'); + } + //@phpstan-ignore-next-line [parent is only defined by the sub classes] + return $qb->getQuery()->getResult(); + } + /** * Finds all nodes without a parent node. They are our root nodes. * @@ -47,7 +69,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit */ public function findRootNodes(): array { - return $this->findBy(['parent' => null], ['name' => 'ASC']); + return $this->findNodesForParent(null); } /** @@ -63,7 +85,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit { $result = []; - $entities = $this->findBy(['parent' => $parent], ['name' => 'ASC']); + $entities = $this->findNodesForParent($parent); foreach ($entities as $entity) { /** @var AbstractStructuralDBElement $entity */ //Make a recursive call to find all children nodes @@ -89,7 +111,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit { $result = []; - $entities = $this->findBy(['parent' => $parent], ['name' => 'ASC']); + $entities = $this->findNodesForParent($parent); $elementIterator = new StructuralDBElementIterator($entities); $recursiveIterator = new RecursiveIteratorIterator($elementIterator, RecursiveIteratorIterator::SELF_FIRST);