Standardized ordering field handling and added validation for short URLs list

This commit is contained in:
Alejandro Celaya
2022-01-09 11:23:27 +01:00
parent d0c9f5a776
commit 2abcaf02e2
12 changed files with 68 additions and 92 deletions

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
final class Ordering
{
private const DEFAULT_DIR = 'ASC';
private function __construct(private ?string $field, private string $dir)
{
}
public static function fromTuple(array $props): self
{
[$field, $dir] = $props;
return new self($field, $dir ?? self::DEFAULT_DIR);
}
public function orderField(): ?string
{
return $this->field;
}
public function orderDirection(): string
{
return $this->dir;
}
public function hasOrderField(): bool
{
return $this->field !== null;
}
}

View File

@@ -1,60 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use function array_pad;
use function explode;
final class ShortUrlsOrdering
{
public const ORDER_BY = 'orderBy';
private const DEFAULT_ORDER_DIRECTION = 'ASC';
private ?string $orderField = null;
private string $orderDirection = self::DEFAULT_ORDER_DIRECTION;
/**
* @throws ValidationException
*/
public static function fromRawData(array $query): self
{
$instance = new self();
$instance->validateAndInit($query);
return $instance;
}
/**
* @throws ValidationException
*/
private function validateAndInit(array $data): void
{
$orderBy = $data[self::ORDER_BY] ?? null;
if ($orderBy === null) {
return;
}
[$field, $dir] = array_pad(explode('-', $orderBy), 2, null);
$this->orderField = $field;
$this->orderDirection = $dir ?? self::DEFAULT_ORDER_DIRECTION;
}
public function orderField(): ?string
{
return $this->orderField;
}
public function orderDirection(): string
{
return $this->orderDirection;
}
public function hasOrderField(): bool
{
return $this->orderField !== null;
}
}

View File

@@ -13,6 +13,7 @@ use function Shlinkio\Shlink\Core\parseDateField;
final class ShortUrlsParams
{
public const ORDERABLE_FIELDS = ['longUrl', 'shortCode', 'dateCreated', 'title', 'visits'];
public const DEFAULT_ITEMS_PER_PAGE = 10;
public const TAGS_MODE_ANY = 'any';
public const TAGS_MODE_ALL = 'all';
@@ -23,7 +24,7 @@ final class ShortUrlsParams
private array $tags;
/** @var self::TAGS_MODE_ANY|self::TAGS_MODE_ALL */
private string $tagsMode = self::TAGS_MODE_ANY;
private ShortUrlsOrdering $orderBy;
private Ordering $orderBy;
private ?DateRange $dateRange;
private function __construct()
@@ -63,7 +64,7 @@ final class ShortUrlsParams
parseDateField($inputFilter->getValue(ShortUrlsParamsInputFilter::START_DATE)),
parseDateField($inputFilter->getValue(ShortUrlsParamsInputFilter::END_DATE)),
);
$this->orderBy = ShortUrlsOrdering::fromRawData($query);
$this->orderBy = Ordering::fromTuple($inputFilter->getValue(ShortUrlsParamsInputFilter::ORDER_BY));
$this->itemsPerPage = (int) (
$inputFilter->getValue(ShortUrlsParamsInputFilter::ITEMS_PER_PAGE) ?? self::DEFAULT_ITEMS_PER_PAGE
);
@@ -90,7 +91,7 @@ final class ShortUrlsParams
return $this->tags;
}
public function orderBy(): ShortUrlsOrdering
public function orderBy(): Ordering
{
return $this->orderBy;
}

View File

@@ -13,9 +13,9 @@ use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Model\Ordering;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
@@ -35,7 +35,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
?string $searchTerm = null,
array $tags = [],
?string $tagsMode = null,
?ShortUrlsOrdering $orderBy = null,
?Ordering $orderBy = null,
?DateRange $dateRange = null,
?Specification $spec = null,
): array {
@@ -53,13 +53,14 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
return $qb->orderBy('s.dateCreated', 'DESC')->getQuery()->getResult();
}
private function processOrderByForList(QueryBuilder $qb, ShortUrlsOrdering $orderBy): array
private function processOrderByForList(QueryBuilder $qb, Ordering $orderBy): array
{
$fieldName = $orderBy->orderField();
$order = $orderBy->orderDirection();
if ($fieldName === 'visits') {
// FIXME This query is inefficient. Debug it.
// FIXME This query is inefficient.
// Diagnostic: It might need to use a sub-query, as done with the tags list query.
$qb->addSelect('COUNT(DISTINCT v) AS totalVisits')
->leftJoin('s.visits', 'v')
->groupBy('s')

View File

@@ -9,9 +9,9 @@ use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepositoryInterfa
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Model\Ordering;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
interface ShortUrlRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface
@@ -22,7 +22,7 @@ interface ShortUrlRepositoryInterface extends ObjectRepository, EntitySpecificat
?string $searchTerm = null,
array $tags = [],
?string $tagsMode = null,
?ShortUrlsOrdering $orderBy = null,
?Ordering $orderBy = null,
?DateRange $dateRange = null,
?Specification $spec = null,
): array;

View File

@@ -21,6 +21,7 @@ class ShortUrlsParamsInputFilter extends InputFilter
public const END_DATE = 'endDate';
public const ITEMS_PER_PAGE = 'itemsPerPage';
public const TAGS_MODE = 'tagsMode';
public const ORDER_BY = 'orderBy';
public function __construct(array $data)
{
@@ -46,5 +47,7 @@ class ShortUrlsParamsInputFilter extends InputFilter
'strict' => InArray::COMPARE_STRICT,
]));
$this->add($tagsMode);
$this->add($this->createOrderByInput(self::ORDER_BY, ShortUrlsParams::ORDERABLE_FIELDS));
}
}