Use natural sorting for trees and others repository functions

This commit is contained in:
Jan Böhmer 2024-06-17 22:33:40 +02:00
parent 9db822eabd
commit 8bb8118d9f
13 changed files with 71 additions and 44 deletions

View file

@ -25,7 +25,7 @@ final class Version20240606203053 extends AbstractMultiPlatformMigration impleme
public function postgreSQLUp(Schema $schema): void public function postgreSQLUp(Schema $schema): void
{ {
//Create a collation for natural sorting //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 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)'); $this->addSql('CREATE UNIQUE INDEX UNIQ_2CAD560E5F37A13B ON api_tokens (token)');

View file

@ -25,6 +25,7 @@ namespace App\Doctrine\Functions;
use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver; use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver;
use Doctrine\DBAL\Platforms\MariaDBPlatform; use Doctrine\DBAL\Platforms\MariaDBPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\Node; use Doctrine\ORM\Query\AST\Node;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
@ -33,7 +34,7 @@ use Doctrine\ORM\Query\TokenType;
class Natsort extends FunctionNode class Natsort extends FunctionNode
{ {
private Node $field; private ?Node $field = null;
public function parse(Parser $parser): void public function parse(Parser $parser): void
{ {
@ -47,15 +48,17 @@ class Natsort extends FunctionNode
public function getSql(SqlWalker $sqlWalker): string public function getSql(SqlWalker $sqlWalker): string
{ {
assert($this->field !== null, 'Field is not set');
$platform = $sqlWalker->getConnection()->getDatabasePlatform(); $platform = $sqlWalker->getConnection()->getDatabasePlatform();
if ($platform instanceof AbstractPostgreSQLDriver) { if ($platform instanceof PostgreSQLPlatform) {
return $this->field->dispatch($sqlWalker) . ' COLLATE numeric'; 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 //For every other platform, return the field as is
return $this->field->dispatch($sqlWalker); return $this->field->dispatch($sqlWalker);

View file

@ -30,11 +30,11 @@ interface PartsContainingRepositoryInterface
* Returns all parts associated with this element. * Returns all parts associated with this element.
* *
* @param object $element the element for which the parts should be determined * @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[] * @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. * Gets the count of the parts associated with this element.

View file

@ -40,11 +40,11 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo
* Returns all parts associated with this element. * Returns all parts associated with this element.
* *
* @param object $element the element for which the parts should be determined * @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[] * @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. * Gets the count of the parts associated with this element.
@ -113,7 +113,7 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo
return $parts; 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) { if (!$element instanceof AbstractPartsContainingDBElement) {
throw new InvalidArgumentException('$element must be an instance of AbstractPartContainingDBElement!'); 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); $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 protected function getPartsCountByField(object $element, string $field_name): int

View file

@ -42,7 +42,7 @@ class NamedDBElementRepository extends DBElementRepository
{ {
$result = []; $result = [];
$entities = $this->findBy([], ['name' => 'ASC']); $entities = $this->getFlatList();
foreach ($entities as $entity) { foreach ($entities as $entity) {
/** @var AbstractNamedDBElement $entity */ /** @var AbstractNamedDBElement $entity */
$node = new TreeViewNode($entity->getName(), null, null); $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[] * @return AbstractNamedDBElement[]
* @phpstan-return array<int, AbstractNamedDBElement> * @phpstan-return array<int, AbstractNamedDBElement>
*/ */
public function getFlatList(): array public function getFlatList(): array
{ {
//All nodes are sorted by name $qb = $this->createQueryBuilder('e');
return $this->findBy([], ['name' => 'ASC']); $q = $qb->select('e')
->orderBy('NATSORT(e.name)', 'ASC')
->getQuery();
return $q->getResult();
} }
} }

View file

@ -90,7 +90,7 @@ class PartRepository extends NamedDBElementRepository
$qb->setParameter('query', '%'.$query.'%'); $qb->setParameter('query', '%'.$query.'%');
$qb->setMaxResults($max_limits); $qb->setMaxResults($max_limits);
$qb->orderBy('part.name', 'ASC'); $qb->orderBy('NATSORT(part.name)', 'ASC');
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }

View file

@ -28,13 +28,13 @@ use InvalidArgumentException;
class CategoryRepository extends AbstractPartsContainingRepository 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) { if (!$element instanceof Category) {
throw new InvalidArgumentException('$element must be an 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 public function getPartsCount(object $element): int

View file

@ -28,13 +28,13 @@ use InvalidArgumentException;
class FootprintRepository extends AbstractPartsContainingRepository 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) { if (!$element instanceof Footprint) {
throw new InvalidArgumentException('$element must be an 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 public function getPartsCount(object $element): int

View file

@ -28,13 +28,13 @@ use InvalidArgumentException;
class ManufacturerRepository extends AbstractPartsContainingRepository 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) { if (!$element instanceof Manufacturer) {
throw new InvalidArgumentException('$element must be an 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 public function getPartsCount(object $element): int

View file

@ -28,13 +28,13 @@ use InvalidArgumentException;
class MeasurementUnitRepository extends AbstractPartsContainingRepository 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) { if (!$element instanceof MeasurementUnit) {
throw new InvalidArgumentException('$element must be an 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 public function getPartsCount(object $element): int

View file

@ -30,12 +30,7 @@ use InvalidArgumentException;
class StorelocationRepository extends AbstractPartsContainingRepository class StorelocationRepository extends AbstractPartsContainingRepository
{ {
/** public function getParts(object $element, string $nameOrderDirection = "ASC"): array
* @param object $element
* @param array $order_by
* @return array
*/
public function getParts(object $element, array $order_by = ['name' => 'ASC']): array
{ {
if (!$element instanceof StorageLocation) { if (!$element instanceof StorageLocation) {
throw new InvalidArgumentException('$element must be an Storelocation!'); throw new InvalidArgumentException('$element must be an Storelocation!');
@ -47,11 +42,9 @@ class StorelocationRepository extends AbstractPartsContainingRepository
->from(Part::class, 'part') ->from(Part::class, 'part')
->leftJoin('part.partLots', 'lots') ->leftJoin('part.partLots', 'lots')
->where('lots.storage_location = ?1') ->where('lots.storage_location = ?1')
->setParameter(1, $element); ->setParameter(1, $element)
->orderBy('NATSORT(part.name)', $nameOrderDirection)
foreach ($order_by as $field => $order) { ;
$qb->addOrderBy('part.'.$field, $order);
}
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }

View file

@ -30,7 +30,7 @@ use InvalidArgumentException;
class SupplierRepository extends AbstractPartsContainingRepository 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) { if (!$element instanceof Supplier) {
throw new InvalidArgumentException('$element must be an Supplier!'); throw new InvalidArgumentException('$element must be an Supplier!');
@ -42,11 +42,9 @@ class SupplierRepository extends AbstractPartsContainingRepository
->from(Part::class, 'part') ->from(Part::class, 'part')
->leftJoin('part.orderdetails', 'orderdetail') ->leftJoin('part.orderdetails', 'orderdetail')
->where('orderdetail.supplier = ?1') ->where('orderdetail.supplier = ?1')
->setParameter(1, $element); ->setParameter(1, $element)
->orderBy('NATSORT(part.name)', $nameOrderDirection)
foreach ($order_by as $field => $order) { ;
$qb->addOrderBy('part.'.$field, $order);
}
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }

View file

@ -40,6 +40,28 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
*/ */
private array $new_entity_cache = []; 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. * Finds all nodes without a parent node. They are our root nodes.
* *
@ -47,7 +69,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
*/ */
public function findRootNodes(): array public function findRootNodes(): array
{ {
return $this->findBy(['parent' => null], ['name' => 'ASC']); return $this->findNodesForParent(null);
} }
/** /**
@ -63,7 +85,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
{ {
$result = []; $result = [];
$entities = $this->findBy(['parent' => $parent], ['name' => 'ASC']); $entities = $this->findNodesForParent($parent);
foreach ($entities as $entity) { foreach ($entities as $entity) {
/** @var AbstractStructuralDBElement $entity */ /** @var AbstractStructuralDBElement $entity */
//Make a recursive call to find all children nodes //Make a recursive call to find all children nodes
@ -89,7 +111,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
{ {
$result = []; $result = [];
$entities = $this->findBy(['parent' => $parent], ['name' => 'ASC']); $entities = $this->findNodesForParent($parent);
$elementIterator = new StructuralDBElementIterator($entities); $elementIterator = new StructuralDBElementIterator($entities);
$recursiveIterator = new RecursiveIteratorIterator($elementIterator, RecursiveIteratorIterator::SELF_FIRST); $recursiveIterator = new RecursiveIteratorIterator($elementIterator, RecursiveIteratorIterator::SELF_FIRST);