mirror of
https://github.com/shlinkio/shlink.git
synced 2024-12-22 15:13:59 -06:00
Enabled search by default domain
This commit is contained in:
parent
54bc169525
commit
dfcac525bc
@ -108,6 +108,7 @@ return [
|
||||
ShortUrl\ShortUrlResolver::class,
|
||||
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class,
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
|
||||
Options\UrlShortenerOptions::class,
|
||||
],
|
||||
Visit\Geolocation\VisitLocator::class => ['em'],
|
||||
Visit\Geolocation\VisitToLocationHelper::class => [IpLocationResolverInterface::class],
|
||||
|
@ -17,16 +17,15 @@ final class ShortUrlsParams
|
||||
public const ORDERABLE_FIELDS = ['longUrl', 'shortCode', 'dateCreated', 'title', 'visits'];
|
||||
public const DEFAULT_ITEMS_PER_PAGE = 10;
|
||||
|
||||
private int $page;
|
||||
private int $itemsPerPage;
|
||||
private ?string $searchTerm;
|
||||
private array $tags;
|
||||
private TagsMode $tagsMode = TagsMode::ANY;
|
||||
private Ordering $orderBy;
|
||||
private ?DateRange $dateRange;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
private function __construct(
|
||||
public readonly int $page,
|
||||
public readonly int $itemsPerPage,
|
||||
public readonly ?string $searchTerm,
|
||||
public readonly array $tags,
|
||||
public readonly Ordering $orderBy,
|
||||
public readonly ?DateRange $dateRange,
|
||||
public readonly TagsMode $tagsMode = TagsMode::ANY,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function emptyInstance(): self
|
||||
@ -38,38 +37,29 @@ final class ShortUrlsParams
|
||||
* @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 = buildDateRange(
|
||||
normalizeOptionalDate($inputFilter->getValue(ShortUrlsParamsInputFilter::START_DATE)),
|
||||
normalizeOptionalDate($inputFilter->getValue(ShortUrlsParamsInputFilter::END_DATE)),
|
||||
return new self(
|
||||
page: (int) ($inputFilter->getValue(ShortUrlsParamsInputFilter::PAGE) ?? 1),
|
||||
itemsPerPage: (int) (
|
||||
$inputFilter->getValue(ShortUrlsParamsInputFilter::ITEMS_PER_PAGE) ?? self::DEFAULT_ITEMS_PER_PAGE
|
||||
),
|
||||
searchTerm: $inputFilter->getValue(ShortUrlsParamsInputFilter::SEARCH_TERM),
|
||||
tags: (array) $inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS),
|
||||
orderBy: Ordering::fromTuple($inputFilter->getValue(ShortUrlsParamsInputFilter::ORDER_BY)),
|
||||
dateRange: buildDateRange(
|
||||
normalizeOptionalDate($inputFilter->getValue(ShortUrlsParamsInputFilter::START_DATE)),
|
||||
normalizeOptionalDate($inputFilter->getValue(ShortUrlsParamsInputFilter::END_DATE)),
|
||||
),
|
||||
tagsMode: self::resolveTagsMode($inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS_MODE)),
|
||||
);
|
||||
$this->orderBy = Ordering::fromTuple($inputFilter->getValue(ShortUrlsParamsInputFilter::ORDER_BY));
|
||||
$this->itemsPerPage = (int) (
|
||||
$inputFilter->getValue(ShortUrlsParamsInputFilter::ITEMS_PER_PAGE) ?? self::DEFAULT_ITEMS_PER_PAGE
|
||||
);
|
||||
$this->tagsMode = $this->resolveTagsMode($inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS_MODE));
|
||||
}
|
||||
|
||||
private function resolveTagsMode(?string $rawTagsMode): TagsMode
|
||||
private static function resolveTagsMode(?string $rawTagsMode): TagsMode
|
||||
{
|
||||
if ($rawTagsMode === null) {
|
||||
return TagsMode::ANY;
|
||||
@ -77,39 +67,4 @@ final class ShortUrlsParams
|
||||
|
||||
return TagsMode::tryFrom($rawTagsMode) ?? TagsMode::ANY;
|
||||
}
|
||||
|
||||
public function page(): int
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
public function itemsPerPage(): int
|
||||
{
|
||||
return $this->itemsPerPage;
|
||||
}
|
||||
|
||||
public function searchTerm(): ?string
|
||||
{
|
||||
return $this->searchTerm;
|
||||
}
|
||||
|
||||
public function tags(): array
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
public function orderBy(): Ordering
|
||||
{
|
||||
return $this->orderBy;
|
||||
}
|
||||
|
||||
public function dateRange(): ?DateRange
|
||||
{
|
||||
return $this->dateRange;
|
||||
}
|
||||
|
||||
public function tagsMode(): TagsMode
|
||||
{
|
||||
return $this->tagsMode;
|
||||
}
|
||||
}
|
||||
|
@ -14,21 +14,28 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
class ShortUrlRepositoryAdapter implements AdapterInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ShortUrlRepositoryInterface $repository,
|
||||
private ShortUrlsParams $params,
|
||||
private ?ApiKey $apiKey,
|
||||
private readonly ShortUrlRepositoryInterface $repository,
|
||||
private readonly ShortUrlsParams $params,
|
||||
private readonly ?ApiKey $apiKey,
|
||||
private readonly string $defaultDomain,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getSlice(int $offset, int $length): iterable
|
||||
{
|
||||
return $this->repository->findList(
|
||||
ShortUrlsListFiltering::fromLimitsAndParams($length, $offset, $this->params, $this->apiKey),
|
||||
);
|
||||
return $this->repository->findList(ShortUrlsListFiltering::fromLimitsAndParams(
|
||||
$length,
|
||||
$offset,
|
||||
$this->params,
|
||||
$this->apiKey,
|
||||
$this->defaultDomain,
|
||||
));
|
||||
}
|
||||
|
||||
public function getNbResults(): int
|
||||
{
|
||||
return $this->repository->countList(ShortUrlsCountFiltering::fromParams($this->params, $this->apiKey));
|
||||
return $this->repository->countList(
|
||||
ShortUrlsCountFiltering::fromParams($this->params, $this->apiKey, $this->defaultDomain),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,44 +9,36 @@ use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
use function str_contains;
|
||||
use function strtolower;
|
||||
|
||||
class ShortUrlsCountFiltering
|
||||
{
|
||||
public readonly bool $searchIncludesDefaultDomain;
|
||||
|
||||
public function __construct(
|
||||
private ?string $searchTerm = null,
|
||||
private array $tags = [],
|
||||
private ?TagsMode $tagsMode = null,
|
||||
private ?DateRange $dateRange = null,
|
||||
private ?ApiKey $apiKey = null,
|
||||
public readonly ?string $searchTerm = null,
|
||||
public readonly array $tags = [],
|
||||
public readonly ?TagsMode $tagsMode = null,
|
||||
public readonly ?DateRange $dateRange = null,
|
||||
public readonly ?ApiKey $apiKey = null,
|
||||
?string $defaultDomain = null,
|
||||
) {
|
||||
$this->searchIncludesDefaultDomain = !empty($searchTerm) && !empty($defaultDomain) && str_contains(
|
||||
strtolower($defaultDomain),
|
||||
strtolower($searchTerm),
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromParams(ShortUrlsParams $params, ?ApiKey $apiKey): self
|
||||
public static function fromParams(ShortUrlsParams $params, ?ApiKey $apiKey, string $defaultDomain): self
|
||||
{
|
||||
return new self($params->searchTerm(), $params->tags(), $params->tagsMode(), $params->dateRange(), $apiKey);
|
||||
}
|
||||
|
||||
public function searchTerm(): ?string
|
||||
{
|
||||
return $this->searchTerm;
|
||||
}
|
||||
|
||||
public function tags(): array
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
public function tagsMode(): ?TagsMode
|
||||
{
|
||||
return $this->tagsMode;
|
||||
}
|
||||
|
||||
public function dateRange(): ?DateRange
|
||||
{
|
||||
return $this->dateRange;
|
||||
}
|
||||
|
||||
public function apiKey(): ?ApiKey
|
||||
{
|
||||
return $this->apiKey;
|
||||
return new self(
|
||||
$params->searchTerm,
|
||||
$params->tags,
|
||||
$params->tagsMode,
|
||||
$params->dateRange,
|
||||
$apiKey,
|
||||
$defaultDomain,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -13,44 +13,36 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
class ShortUrlsListFiltering extends ShortUrlsCountFiltering
|
||||
{
|
||||
public function __construct(
|
||||
private ?int $limit,
|
||||
private ?int $offset,
|
||||
private Ordering $orderBy,
|
||||
public readonly ?int $limit,
|
||||
public readonly ?int $offset,
|
||||
public readonly Ordering $orderBy,
|
||||
?string $searchTerm = null,
|
||||
array $tags = [],
|
||||
?TagsMode $tagsMode = null,
|
||||
?DateRange $dateRange = null,
|
||||
?ApiKey $apiKey = null,
|
||||
?string $defaultDomain = null,
|
||||
) {
|
||||
parent::__construct($searchTerm, $tags, $tagsMode, $dateRange, $apiKey);
|
||||
parent::__construct($searchTerm, $tags, $tagsMode, $dateRange, $apiKey, $defaultDomain);
|
||||
}
|
||||
|
||||
public static function fromLimitsAndParams(int $limit, int $offset, ShortUrlsParams $params, ?ApiKey $apiKey): self
|
||||
{
|
||||
public static function fromLimitsAndParams(
|
||||
int $limit,
|
||||
int $offset,
|
||||
ShortUrlsParams $params,
|
||||
?ApiKey $apiKey,
|
||||
string $defaultDomain,
|
||||
): self {
|
||||
return new self(
|
||||
$limit,
|
||||
$offset,
|
||||
$params->orderBy(),
|
||||
$params->searchTerm(),
|
||||
$params->tags(),
|
||||
$params->tagsMode(),
|
||||
$params->dateRange(),
|
||||
$params->orderBy,
|
||||
$params->searchTerm,
|
||||
$params->tags,
|
||||
$params->tagsMode,
|
||||
$params->dateRange,
|
||||
$apiKey,
|
||||
$defaultDomain,
|
||||
);
|
||||
}
|
||||
|
||||
public function offset(): ?int
|
||||
{
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
public function limit(): ?int
|
||||
{
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
public function orderBy(): Ordering
|
||||
{
|
||||
return $this->orderBy;
|
||||
}
|
||||
}
|
||||
|
@ -33,12 +33,12 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
||||
{
|
||||
$qb = $this->createListQueryBuilder($filtering);
|
||||
$qb->select('DISTINCT s')
|
||||
->setMaxResults($filtering->limit())
|
||||
->setFirstResult($filtering->offset());
|
||||
->setMaxResults($filtering->limit)
|
||||
->setFirstResult($filtering->offset);
|
||||
|
||||
// In case the ordering has been specified, the query could be more complex. Process it
|
||||
if ($filtering->orderBy()->hasOrderField()) {
|
||||
return $this->processOrderByForList($qb, $filtering->orderBy());
|
||||
if ($filtering->orderBy->hasOrderField()) {
|
||||
return $this->processOrderByForList($qb, $filtering->orderBy);
|
||||
}
|
||||
|
||||
// With no explicit order by, fallback to dateCreated-DESC
|
||||
@ -83,7 +83,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
||||
$qb->from(ShortUrl::class, 's')
|
||||
->where('1=1');
|
||||
|
||||
$dateRange = $filtering->dateRange();
|
||||
$dateRange = $filtering->dateRange;
|
||||
if ($dateRange?->startDate !== null) {
|
||||
$qb->andWhere($qb->expr()->gte('s.dateCreated', ':startDate'));
|
||||
$qb->setParameter('startDate', $dateRange->startDate, ChronosDateTimeType::CHRONOS_DATETIME);
|
||||
@ -93,8 +93,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
||||
$qb->setParameter('endDate', $dateRange->endDate, ChronosDateTimeType::CHRONOS_DATETIME);
|
||||
}
|
||||
|
||||
$searchTerm = $filtering->searchTerm();
|
||||
$tags = $filtering->tags();
|
||||
$searchTerm = $filtering->searchTerm;
|
||||
$tags = $filtering->tags;
|
||||
// Apply search term to every searchable field if not empty
|
||||
if (! empty($searchTerm)) {
|
||||
// Left join with tags only if no tags were provided. In case of tags, an inner join will be done later
|
||||
@ -110,8 +110,13 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
||||
$qb->expr()->like('d.authority', ':searchPattern'),
|
||||
];
|
||||
|
||||
// Include default domain in search if provided
|
||||
if ($filtering->searchIncludesDefaultDomain) {
|
||||
$conditions[] = $qb->expr()->isNull('s.domain');
|
||||
}
|
||||
|
||||
// Apply tag conditions, only when not filtering by all provided tags
|
||||
$tagsMode = $filtering->tagsMode() ?? TagsMode::ANY;
|
||||
$tagsMode = $filtering->tagsMode ?? TagsMode::ANY;
|
||||
if (empty($tags) || $tagsMode === TagsMode::ANY) {
|
||||
$conditions[] = $qb->expr()->like('t.name', ':searchPattern');
|
||||
}
|
||||
@ -123,13 +128,13 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
||||
|
||||
// Filter by tags if provided
|
||||
if (! empty($tags)) {
|
||||
$tagsMode = $filtering->tagsMode() ?? TagsMode::ANY;
|
||||
$tagsMode = $filtering->tagsMode ?? TagsMode::ANY;
|
||||
$tagsMode === TagsMode::ANY
|
||||
? $qb->join('s.tags', 't')->andWhere($qb->expr()->in('t.name', $tags))
|
||||
: $this->joinAllTags($qb, $tags);
|
||||
}
|
||||
|
||||
$this->applySpecification($qb, $filtering->apiKey()?->spec(), 's');
|
||||
$this->applySpecification($qb, $filtering->apiKey?->spec(), 's');
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ use Doctrine\ORM;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
||||
@ -25,6 +26,7 @@ class ShortUrlService implements ShortUrlServiceInterface
|
||||
private readonly ShortUrlResolverInterface $urlResolver,
|
||||
private readonly ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
||||
private readonly ShortUrlRelationResolverInterface $relationResolver,
|
||||
private readonly UrlShortenerOptions $urlShortenerOptions,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -35,9 +37,10 @@ class ShortUrlService implements ShortUrlServiceInterface
|
||||
{
|
||||
/** @var ShortUrlRepository $repo */
|
||||
$repo = $this->em->getRepository(ShortUrl::class);
|
||||
$paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $params, $apiKey));
|
||||
$paginator->setMaxPerPage($params->itemsPerPage())
|
||||
->setCurrentPage($params->page());
|
||||
$defaultDomain = $this->urlShortenerOptions->domain['hostname'] ?? '';
|
||||
$paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $params, $apiKey, $defaultDomain));
|
||||
$paginator->setMaxPerPage($params->itemsPerPage)
|
||||
->setCurrentPage($params->page);
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
@ -42,9 +42,9 @@ class ShortUrlRepositoryAdapterTest extends TestCase
|
||||
'endDate' => $endDate,
|
||||
'orderBy' => $orderBy,
|
||||
]);
|
||||
$adapter = new ShortUrlRepositoryAdapter($this->repo, $params, null);
|
||||
$orderBy = $params->orderBy();
|
||||
$dateRange = $params->dateRange();
|
||||
$adapter = new ShortUrlRepositoryAdapter($this->repo, $params, null, '');
|
||||
$orderBy = $params->orderBy;
|
||||
$dateRange = $params->dateRange;
|
||||
|
||||
$this->repo->expects($this->once())->method('findList')->with(
|
||||
new ShortUrlsListFiltering(10, 5, $orderBy, $searchTerm, $tags, TagsMode::ANY, $dateRange),
|
||||
@ -70,8 +70,8 @@ class ShortUrlRepositoryAdapterTest extends TestCase
|
||||
'endDate' => $endDate,
|
||||
]);
|
||||
$apiKey = ApiKey::create();
|
||||
$adapter = new ShortUrlRepositoryAdapter($this->repo, $params, $apiKey);
|
||||
$dateRange = $params->dateRange();
|
||||
$adapter = new ShortUrlRepositoryAdapter($this->repo, $params, $apiKey, '');
|
||||
$dateRange = $params->dateRange;
|
||||
|
||||
$this->repo->expects($this->once())->method('countList')->with(
|
||||
new ShortUrlsCountFiltering($searchTerm, $tags, TagsMode::ANY, $dateRange, $apiKey),
|
||||
|
@ -8,6 +8,7 @@ use Cake\Chronos\Chronos;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
||||
@ -45,6 +46,7 @@ class ShortUrlServiceTest extends TestCase
|
||||
$this->urlResolver,
|
||||
$this->titleResolutionHelper,
|
||||
new SimpleShortUrlRelationResolver(),
|
||||
new UrlShortenerOptions(),
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user