mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-10 09:13:11 +08:00
Created DTOs with implicit validation to wrap short URLs lists params
This commit is contained in:
63
module/Core/src/Model/ShortUrlsOrdering.php
Normal file
63
module/Core/src/Model/ShortUrlsOrdering.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Model;
|
||||
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use function key;
|
||||
|
||||
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
|
||||
{
|
||||
/** @var string|array|null $orderBy */
|
||||
$orderBy = $data[self::ORDER_BY] ?? null;
|
||||
if ($orderBy === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isArray = is_array($orderBy);
|
||||
if (! $isArray && $orderBy !== null && ! is_string($orderBy)) {
|
||||
throw ValidationException::fromArray([
|
||||
'orderBy' => '"Order by" must be an array, string or null',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->orderField = $isArray ? key($orderBy) : $orderBy;
|
||||
$this->orderDirection = $isArray ? $orderBy[$this->orderField] : self::DEFAULT_ORDER_DIRECTION;
|
||||
}
|
||||
|
||||
public function orderField(): ?string
|
||||
{
|
||||
return $this->orderField;
|
||||
}
|
||||
|
||||
public function orderDirection(): string
|
||||
{
|
||||
return $this->orderDirection;
|
||||
}
|
||||
}
|
||||
85
module/Core/src/Model/ShortUrlsParams.php
Normal file
85
module/Core/src/Model/ShortUrlsParams.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Model;
|
||||
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlsParamsInputFilter;
|
||||
|
||||
use function Shlinkio\Shlink\Core\parseDateField;
|
||||
|
||||
final class ShortUrlsParams
|
||||
{
|
||||
private int $page;
|
||||
private ?string $searchTerm;
|
||||
private array $tags;
|
||||
private ShortUrlsOrdering $orderBy;
|
||||
private ?DateRange $dateRange;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function emptyInstance(): self
|
||||
{
|
||||
return self::fromRawData([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public static function fromRawData(array $query): self
|
||||
{
|
||||
$instance = new self();
|
||||
$instance->validateAndInit($query);
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validateAndInit(array $query): void
|
||||
{
|
||||
$inputFilter = new ShortUrlsParamsInputFilter($query);
|
||||
if (! $inputFilter->isValid()) {
|
||||
throw ValidationException::fromInputFilter($inputFilter);
|
||||
}
|
||||
|
||||
$this->page = (int) ($inputFilter->getValue(ShortUrlsParamsInputFilter::PAGE) ?? 1);
|
||||
$this->searchTerm = $inputFilter->getValue(ShortUrlsParamsInputFilter::SEARCH_TERM);
|
||||
$this->tags = (array) $inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS);
|
||||
$this->dateRange = new DateRange(
|
||||
parseDateField($inputFilter->getValue(ShortUrlsParamsInputFilter::START_DATE)),
|
||||
parseDateField($inputFilter->getValue(ShortUrlsParamsInputFilter::END_DATE)),
|
||||
);
|
||||
$this->orderBy = ShortUrlsOrdering::fromRawData($query);
|
||||
}
|
||||
|
||||
public function page(): int
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
public function searchTerm(): ?string
|
||||
{
|
||||
return $this->searchTerm;
|
||||
}
|
||||
|
||||
public function tags(): array
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
public function orderBy(): ShortUrlsOrdering
|
||||
{
|
||||
return $this->orderBy;
|
||||
}
|
||||
|
||||
public function dateRange(): ?DateRange
|
||||
{
|
||||
return $this->dateRange;
|
||||
}
|
||||
}
|
||||
@@ -5,38 +5,20 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
|
||||
|
||||
use Laminas\Paginator\Adapter\AdapterInterface;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||
|
||||
use function strip_tags;
|
||||
use function trim;
|
||||
|
||||
class ShortUrlRepositoryAdapter implements AdapterInterface
|
||||
{
|
||||
public const ITEMS_PER_PAGE = 10;
|
||||
|
||||
private ShortUrlRepositoryInterface $repository;
|
||||
private ?string $searchTerm;
|
||||
/** @var null|array|string */
|
||||
private $orderBy;
|
||||
private array $tags;
|
||||
private ?DateRange $dateRange;
|
||||
private ShortUrlsParams $params;
|
||||
|
||||
/**
|
||||
* @param string|array|null $orderBy
|
||||
*/
|
||||
public function __construct(
|
||||
ShortUrlRepositoryInterface $repository,
|
||||
?string $searchTerm = null,
|
||||
array $tags = [],
|
||||
$orderBy = null,
|
||||
?DateRange $dateRange = null
|
||||
) {
|
||||
public function __construct(ShortUrlRepositoryInterface $repository, ShortUrlsParams $params)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->searchTerm = $searchTerm !== null ? trim(strip_tags($searchTerm)) : null;
|
||||
$this->orderBy = $orderBy;
|
||||
$this->tags = $tags;
|
||||
$this->dateRange = $dateRange;
|
||||
$this->params = $params;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,10 +32,10 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
|
||||
return $this->repository->findList(
|
||||
$itemCountPerPage,
|
||||
$offset,
|
||||
$this->searchTerm,
|
||||
$this->tags,
|
||||
$this->orderBy,
|
||||
$this->dateRange,
|
||||
$this->params->searchTerm(),
|
||||
$this->params->tags(),
|
||||
$this->params->orderBy(),
|
||||
$this->params->dateRange(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,6 +50,10 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return $this->repository->countList($this->searchTerm, $this->tags, $this->dateRange);
|
||||
return $this->repository->countList(
|
||||
$this->params->searchTerm(),
|
||||
$this->params->tags(),
|
||||
$this->params->dateRange(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,16 @@ use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
|
||||
|
||||
use function array_column;
|
||||
use function array_key_exists;
|
||||
use function Functional\contains;
|
||||
use function is_array;
|
||||
use function key;
|
||||
|
||||
class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @param string|array|null $orderBy
|
||||
* @return ShortUrl[]
|
||||
*/
|
||||
public function findList(
|
||||
@@ -27,7 +25,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
|
||||
?int $offset = null,
|
||||
?string $searchTerm = null,
|
||||
array $tags = [],
|
||||
$orderBy = null,
|
||||
?ShortUrlsOrdering $orderBy = null,
|
||||
?DateRange $dateRange = null
|
||||
): array {
|
||||
$qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange);
|
||||
@@ -51,14 +49,10 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array|null $orderBy
|
||||
*/
|
||||
private function processOrderByForList(QueryBuilder $qb, $orderBy): array
|
||||
private function processOrderByForList(QueryBuilder $qb, ShortUrlsOrdering $orderBy): array
|
||||
{
|
||||
$isArray = is_array($orderBy);
|
||||
$fieldName = $isArray ? key($orderBy) : $orderBy;
|
||||
$order = $isArray ? $orderBy[$fieldName] : 'ASC';
|
||||
$fieldName = $orderBy->orderField();
|
||||
$order = $orderBy->orderDirection();
|
||||
|
||||
if (contains(['visits', 'visitsCount', 'visitCount'], $fieldName)) {
|
||||
$qb->addSelect('COUNT(DISTINCT v) AS totalVisits')
|
||||
|
||||
@@ -7,18 +7,16 @@ namespace Shlinkio\Shlink\Core\Repository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
|
||||
|
||||
interface ShortUrlRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
/**
|
||||
* @param string|array|null $orderBy
|
||||
*/
|
||||
public function findList(
|
||||
?int $limit = null,
|
||||
?int $offset = null,
|
||||
?string $searchTerm = null,
|
||||
array $tags = [],
|
||||
$orderBy = null,
|
||||
?ShortUrlsOrdering $orderBy = null,
|
||||
?DateRange $dateRange = null
|
||||
): array;
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Doctrine\ORM;
|
||||
use Laminas\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
||||
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\FindShortCodeTrait;
|
||||
@@ -28,23 +28,15 @@ class ShortUrlService implements ShortUrlServiceInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @param array|string|null $orderBy
|
||||
*
|
||||
* @return ShortUrl[]|Paginator
|
||||
*/
|
||||
public function listShortUrls(
|
||||
int $page = 1,
|
||||
?string $searchQuery = null,
|
||||
array $tags = [],
|
||||
$orderBy = null,
|
||||
?DateRange $dateRange = null
|
||||
) {
|
||||
public function listShortUrls(ShortUrlsParams $params): Paginator
|
||||
{
|
||||
/** @var ShortUrlRepository $repo */
|
||||
$repo = $this->em->getRepository(ShortUrl::class);
|
||||
$paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $searchQuery, $tags, $orderBy, $dateRange));
|
||||
$paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $params));
|
||||
$paginator->setItemCountPerPage(ShortUrlRepositoryAdapter::ITEMS_PER_PAGE)
|
||||
->setCurrentPageNumber($page);
|
||||
->setCurrentPageNumber($params->page());
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
@@ -5,26 +5,17 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Laminas\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
||||
|
||||
interface ShortUrlServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @param array|string|null $orderBy
|
||||
*
|
||||
* @return ShortUrl[]|Paginator
|
||||
*/
|
||||
public function listShortUrls(
|
||||
int $page = 1,
|
||||
?string $searchQuery = null,
|
||||
array $tags = [],
|
||||
$orderBy = null,
|
||||
?DateRange $dateRange = null
|
||||
);
|
||||
public function listShortUrls(ShortUrlsParams $params): Paginator;
|
||||
|
||||
/**
|
||||
* @param string[] $tags
|
||||
|
||||
55
module/Core/src/Validation/ShortUrlsParamsInputFilter.php
Normal file
55
module/Core/src/Validation/ShortUrlsParamsInputFilter.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Validation;
|
||||
|
||||
use DateTime;
|
||||
use Laminas\Filter;
|
||||
use Laminas\InputFilter\ArrayInput;
|
||||
use Laminas\InputFilter\InputFilter;
|
||||
use Laminas\Validator;
|
||||
use Shlinkio\Shlink\Common\Validation;
|
||||
|
||||
class ShortUrlsParamsInputFilter extends InputFilter
|
||||
{
|
||||
use Validation\InputFactoryTrait;
|
||||
|
||||
public const PAGE = 'page';
|
||||
public const SEARCH_TERM = 'searchTerm';
|
||||
public const TAGS = 'tags';
|
||||
public const START_DATE = 'startDate';
|
||||
public const END_DATE = 'endDate';
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->initialize();
|
||||
$this->setData($data);
|
||||
}
|
||||
|
||||
private function initialize(): void
|
||||
{
|
||||
$startDate = $this->createInput(self::START_DATE, false);
|
||||
$startDate->getValidatorChain()->attach(new Validator\Date(['format' => DateTime::ATOM]));
|
||||
$this->add($startDate);
|
||||
|
||||
$endDate = $this->createInput(self::END_DATE, false);
|
||||
$endDate->getValidatorChain()->attach(new Validator\Date(['format' => DateTime::ATOM]));
|
||||
$this->add($endDate);
|
||||
|
||||
$this->add($this->createInput(self::SEARCH_TERM, false));
|
||||
|
||||
$page = $this->createInput(self::PAGE, false);
|
||||
$page->getValidatorChain()->attach(new Validator\Digits())
|
||||
->attach(new Validator\GreaterThan(['min' => 1, 'inclusive' => true]));
|
||||
$this->add($page);
|
||||
|
||||
$tags = new ArrayInput(self::TAGS);
|
||||
$tags->setRequired(false)
|
||||
->getFilterChain()->attach(new Filter\StripTags())
|
||||
->attach(new Filter\StringTrim())
|
||||
->attach(new Filter\StringToLower())
|
||||
->attach(new Validation\SluggerFilter());
|
||||
$this->add($tags);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user