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
{
//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)');

View file

@ -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);

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

@ -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

View file

@ -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<int, AbstractNamedDBElement>
*/
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();
}
}

View file

@ -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();
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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);