From dfcac525bc23cc259c482721784765598d30b31f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 8 Dec 2022 20:21:14 +0100 Subject: [PATCH] Enabled search by default domain --- module/Core/config/dependencies.config.php | 1 + .../src/ShortUrl/Model/ShortUrlsParams.php | 91 +++++-------------- .../Adapter/ShortUrlRepositoryAdapter.php | 21 +++-- .../Persistence/ShortUrlsCountFiltering.php | 56 +++++------- .../Persistence/ShortUrlsListFiltering.php | 44 ++++----- .../Repository/ShortUrlRepository.php | 25 +++-- module/Core/src/ShortUrl/ShortUrlService.php | 9 +- .../Adapter/ShortUrlRepositoryAdapterTest.php | 10 +- .../test/ShortUrl/ShortUrlServiceTest.php | 2 + 9 files changed, 108 insertions(+), 151 deletions(-) diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index a7ebd1b7..5f51c2d7 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -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], diff --git a/module/Core/src/ShortUrl/Model/ShortUrlsParams.php b/module/Core/src/ShortUrl/Model/ShortUrlsParams.php index 6f8b0f47..14e88132 100644 --- a/module/Core/src/ShortUrl/Model/ShortUrlsParams.php +++ b/module/Core/src/ShortUrl/Model/ShortUrlsParams.php @@ -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; - } } diff --git a/module/Core/src/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapter.php b/module/Core/src/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapter.php index f576106f..d88d8b81 100644 --- a/module/Core/src/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapter.php +++ b/module/Core/src/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapter.php @@ -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), + ); } } diff --git a/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php b/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php index c4b07281..2d1b1f21 100644 --- a/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php +++ b/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php @@ -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, + ); } } diff --git a/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php b/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php index 6e32d93d..dd7eb0aa 100644 --- a/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php +++ b/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php @@ -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; - } } diff --git a/module/Core/src/ShortUrl/Repository/ShortUrlRepository.php b/module/Core/src/ShortUrl/Repository/ShortUrlRepository.php index 8271b5f9..ec2f7a94 100644 --- a/module/Core/src/ShortUrl/Repository/ShortUrlRepository.php +++ b/module/Core/src/ShortUrl/Repository/ShortUrlRepository.php @@ -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; } diff --git a/module/Core/src/ShortUrl/ShortUrlService.php b/module/Core/src/ShortUrl/ShortUrlService.php index d4ab984a..07549e5b 100644 --- a/module/Core/src/ShortUrl/ShortUrlService.php +++ b/module/Core/src/ShortUrl/ShortUrlService.php @@ -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; } diff --git a/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php b/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php index 4cbc9eae..aa8efbd5 100644 --- a/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php +++ b/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php @@ -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), diff --git a/module/Core/test/ShortUrl/ShortUrlServiceTest.php b/module/Core/test/ShortUrl/ShortUrlServiceTest.php index 876d322b..7b27f4e4 100644 --- a/module/Core/test/ShortUrl/ShortUrlServiceTest.php +++ b/module/Core/test/ShortUrl/ShortUrlServiceTest.php @@ -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(), ); }