mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-23 21:27:44 +03:00
Merge pull request #1628 from acelaya-forks/feature/split-repos-poc
Split short URL listing capabilities on its own repo and service
This commit is contained in:
commit
425d8f0a3f
22 changed files with 654 additions and 536 deletions
|
@ -45,7 +45,7 @@
|
||||||
"php-middleware/request-id": "^4.1",
|
"php-middleware/request-id": "^4.1",
|
||||||
"pugx/shortid-php": "^1.1",
|
"pugx/shortid-php": "^1.1",
|
||||||
"ramsey/uuid": "^4.5",
|
"ramsey/uuid": "^4.5",
|
||||||
"shlinkio/shlink-common": "dev-main#8d06f0e as 5.2",
|
"shlinkio/shlink-common": "dev-main#e2a5bb7 as 5.2",
|
||||||
"shlinkio/shlink-config": "dev-main#96c81fb as 2.3",
|
"shlinkio/shlink-config": "dev-main#96c81fb as 2.3",
|
||||||
"shlinkio/shlink-event-dispatcher": "^2.6",
|
"shlinkio/shlink-event-dispatcher": "^2.6",
|
||||||
"shlinkio/shlink-importer": "dev-main#c97662b as 5.0",
|
"shlinkio/shlink-importer": "dev-main#c97662b as 5.0",
|
||||||
|
|
|
@ -84,7 +84,7 @@ return [
|
||||||
],
|
],
|
||||||
Command\ShortUrl\ResolveUrlCommand::class => [ShortUrl\ShortUrlResolver::class],
|
Command\ShortUrl\ResolveUrlCommand::class => [ShortUrl\ShortUrlResolver::class],
|
||||||
Command\ShortUrl\ListShortUrlsCommand::class => [
|
Command\ShortUrl\ListShortUrlsCommand::class => [
|
||||||
ShortUrl\ShortUrlService::class,
|
ShortUrl\ShortUrlListService::class,
|
||||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||||
],
|
],
|
||||||
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||||
|
|
|
@ -15,7 +15,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlsParamsInputFilter;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlsParamsInputFilter;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlServiceInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
@ -39,7 +39,7 @@ class ListShortUrlsCommand extends Command
|
||||||
private readonly EndDateOption $endDateOption;
|
private readonly EndDateOption $endDateOption;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ShortUrlServiceInterface $shortUrlService,
|
private readonly ShortUrlListServiceInterface $shortUrlService,
|
||||||
private readonly DataTransformerInterface $transformer,
|
private readonly DataTransformerInterface $transformer,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
|
@ -15,7 +15,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlServiceInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
@ -30,11 +30,11 @@ class ListShortUrlsCommandTest extends TestCase
|
||||||
use CliTestUtilsTrait;
|
use CliTestUtilsTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private MockObject & ShortUrlServiceInterface $shortUrlService;
|
private MockObject & ShortUrlListServiceInterface $shortUrlService;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->shortUrlService = $this->createMock(ShortUrlServiceInterface::class);
|
$this->shortUrlService = $this->createMock(ShortUrlListServiceInterface::class);
|
||||||
$command = new ListShortUrlsCommand($this->shortUrlService, new ShortUrlDataTransformer(
|
$command = new ListShortUrlsCommand($this->shortUrlService, new ShortUrlDataTransformer(
|
||||||
new ShortUrlStringifier([]),
|
new ShortUrlStringifier([]),
|
||||||
));
|
));
|
||||||
|
|
|
@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core;
|
||||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Shlinkio\Shlink\Common\Doctrine\EntityRepositoryFactory;
|
||||||
use Shlinkio\Shlink\Config\Factory\ValinorConfigFactory;
|
use Shlinkio\Shlink\Config\Factory\ValinorConfigFactory;
|
||||||
use Shlinkio\Shlink\Core\ErrorHandler;
|
use Shlinkio\Shlink\Core\ErrorHandler;
|
||||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||||
|
@ -34,6 +35,7 @@ return [
|
||||||
|
|
||||||
ShortUrl\UrlShortener::class => ConfigAbstractFactory::class,
|
ShortUrl\UrlShortener::class => ConfigAbstractFactory::class,
|
||||||
ShortUrl\ShortUrlService::class => ConfigAbstractFactory::class,
|
ShortUrl\ShortUrlService::class => ConfigAbstractFactory::class,
|
||||||
|
ShortUrl\ShortUrlListService::class => ConfigAbstractFactory::class,
|
||||||
ShortUrl\DeleteShortUrlService::class => ConfigAbstractFactory::class,
|
ShortUrl\DeleteShortUrlService::class => ConfigAbstractFactory::class,
|
||||||
ShortUrl\ShortUrlResolver::class => ConfigAbstractFactory::class,
|
ShortUrl\ShortUrlResolver::class => ConfigAbstractFactory::class,
|
||||||
ShortUrl\Helper\ShortCodeUniquenessHelper::class => ConfigAbstractFactory::class,
|
ShortUrl\Helper\ShortCodeUniquenessHelper::class => ConfigAbstractFactory::class,
|
||||||
|
@ -44,6 +46,10 @@ return [
|
||||||
ShortUrl\Transformer\ShortUrlDataTransformer::class => ConfigAbstractFactory::class,
|
ShortUrl\Transformer\ShortUrlDataTransformer::class => ConfigAbstractFactory::class,
|
||||||
ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => ConfigAbstractFactory::class,
|
ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => ConfigAbstractFactory::class,
|
||||||
ShortUrl\Middleware\TrimTrailingSlashMiddleware::class => ConfigAbstractFactory::class,
|
ShortUrl\Middleware\TrimTrailingSlashMiddleware::class => ConfigAbstractFactory::class,
|
||||||
|
ShortUrl\Repository\ShortUrlListRepository::class => [
|
||||||
|
EntityRepositoryFactory::class,
|
||||||
|
ShortUrl\Entity\ShortUrl::class,
|
||||||
|
],
|
||||||
|
|
||||||
Tag\TagService::class => ConfigAbstractFactory::class,
|
Tag\TagService::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
|
@ -108,6 +114,9 @@ return [
|
||||||
ShortUrl\ShortUrlResolver::class,
|
ShortUrl\ShortUrlResolver::class,
|
||||||
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class,
|
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class,
|
||||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
|
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
|
||||||
|
],
|
||||||
|
ShortUrl\ShortUrlListService::class => [
|
||||||
|
ShortUrl\Repository\ShortUrlListRepository::class,
|
||||||
Options\UrlShortenerOptions::class,
|
Options\UrlShortenerOptions::class,
|
||||||
],
|
],
|
||||||
Visit\Geolocation\VisitLocator::class => ['em'],
|
Visit\Geolocation\VisitLocator::class => ['em'],
|
||||||
|
|
|
@ -8,13 +8,13 @@ use Pagerfanta\Adapter\AdapterInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering;
|
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering;
|
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepositoryInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlListRepositoryInterface;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
class ShortUrlRepositoryAdapter implements AdapterInterface
|
class ShortUrlRepositoryAdapter implements AdapterInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ShortUrlRepositoryInterface $repository,
|
private readonly ShortUrlListRepositoryInterface $repository,
|
||||||
private readonly ShortUrlsParams $params,
|
private readonly ShortUrlsParams $params,
|
||||||
private readonly ?ApiKey $apiKey,
|
private readonly ?ApiKey $apiKey,
|
||||||
private readonly string $defaultDomain,
|
private readonly string $defaultDomain,
|
||||||
|
|
165
module/Core/src/ShortUrl/Repository/ShortUrlListRepository.php
Normal file
165
module/Core/src/ShortUrl/Repository/ShortUrlListRepository.php
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\ShortUrl\Repository;
|
||||||
|
|
||||||
|
use Cake\Chronos\Chronos;
|
||||||
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
|
||||||
|
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
|
|
||||||
|
use function array_column;
|
||||||
|
use function Functional\contains;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
class ShortUrlListRepository extends EntitySpecificationRepository implements ShortUrlListRepositoryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return ShortUrl[]
|
||||||
|
*/
|
||||||
|
public function findList(ShortUrlsListFiltering $filtering): array
|
||||||
|
{
|
||||||
|
$qb = $this->createListQueryBuilder($filtering);
|
||||||
|
$qb->select('DISTINCT s')
|
||||||
|
->setMaxResults($filtering->limit)
|
||||||
|
->setFirstResult($filtering->offset);
|
||||||
|
|
||||||
|
// In case the ordering has been specified, the query could be more complex. Process it
|
||||||
|
$this->processOrderByForList($qb, $filtering);
|
||||||
|
|
||||||
|
$result = $qb->getQuery()->getResult();
|
||||||
|
if ($filtering->orderBy->field === 'visits') {
|
||||||
|
return array_column($result, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOrderByForList(QueryBuilder $qb, ShortUrlsListFiltering $filtering): void
|
||||||
|
{
|
||||||
|
// With no explicit order by, fallback to dateCreated-DESC
|
||||||
|
if (! $filtering->orderBy->hasOrderField()) {
|
||||||
|
$qb->orderBy('s.dateCreated', 'DESC');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldName = $filtering->orderBy->field;
|
||||||
|
$order = $filtering->orderBy->direction;
|
||||||
|
|
||||||
|
if ($fieldName === 'visits') {
|
||||||
|
// 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)')
|
||||||
|
->leftJoin('s.visits', 'v')
|
||||||
|
->groupBy('s')
|
||||||
|
->orderBy('COUNT(DISTINCT v)', $order);
|
||||||
|
} elseif (contains(['longUrl', 'shortCode', 'dateCreated', 'title'], $fieldName)) {
|
||||||
|
$qb->orderBy('s.' . $fieldName, $order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countList(ShortUrlsCountFiltering $filtering): int
|
||||||
|
{
|
||||||
|
$qb = $this->createListQueryBuilder($filtering);
|
||||||
|
$qb->select('COUNT(DISTINCT s)');
|
||||||
|
|
||||||
|
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createListQueryBuilder(ShortUrlsCountFiltering $filtering): QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||||
|
$qb->from(ShortUrl::class, 's')
|
||||||
|
->where('1=1');
|
||||||
|
|
||||||
|
$dateRange = $filtering->dateRange;
|
||||||
|
if ($dateRange?->startDate !== null) {
|
||||||
|
$qb->andWhere($qb->expr()->gte('s.dateCreated', ':startDate'));
|
||||||
|
$qb->setParameter('startDate', $dateRange->startDate, ChronosDateTimeType::CHRONOS_DATETIME);
|
||||||
|
}
|
||||||
|
if ($dateRange?->endDate !== null) {
|
||||||
|
$qb->andWhere($qb->expr()->lte('s.dateCreated', ':endDate'));
|
||||||
|
$qb->setParameter('endDate', $dateRange->endDate, ChronosDateTimeType::CHRONOS_DATETIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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
|
||||||
|
if (empty($tags)) {
|
||||||
|
$qb->leftJoin('s.tags', 't');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply general search conditions
|
||||||
|
$conditions = [
|
||||||
|
$qb->expr()->like('s.longUrl', ':searchPattern'),
|
||||||
|
$qb->expr()->like('s.shortCode', ':searchPattern'),
|
||||||
|
$qb->expr()->like('s.title', ':searchPattern'),
|
||||||
|
$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;
|
||||||
|
if (empty($tags) || $tagsMode === TagsMode::ANY) {
|
||||||
|
$conditions[] = $qb->expr()->like('t.name', ':searchPattern');
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb->leftJoin('s.domain', 'd')
|
||||||
|
->andWhere($qb->expr()->orX(...$conditions))
|
||||||
|
->setParameter('searchPattern', '%' . $searchTerm . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by tags if provided
|
||||||
|
if (! empty($tags)) {
|
||||||
|
$tagsMode = $filtering->tagsMode ?? TagsMode::ANY;
|
||||||
|
$tagsMode === TagsMode::ANY
|
||||||
|
? $qb->join('s.tags', 't')->andWhere($qb->expr()->in('t.name', $tags))
|
||||||
|
: $this->joinAllTags($qb, $tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($filtering->excludeMaxVisitsReached) {
|
||||||
|
$qb->andWhere($qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull('s.maxVisits'),
|
||||||
|
$qb->expr()->gt(
|
||||||
|
's.maxVisits',
|
||||||
|
sprintf('(SELECT COUNT(innerV.id) FROM %s as innerV WHERE innerV.shortUrl=s)', Visit::class),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($filtering->excludePastValidUntil) {
|
||||||
|
$qb
|
||||||
|
->andWhere($qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull('s.validUntil'),
|
||||||
|
$qb->expr()->gte('s.validUntil', ':minValidUntil'),
|
||||||
|
))
|
||||||
|
->setParameter('minValidUntil', Chronos::now()->toDateTimeString());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->applySpecification($qb, $filtering->apiKey?->spec(), 's');
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function joinAllTags(QueryBuilder $qb, array $tags): void
|
||||||
|
{
|
||||||
|
foreach ($tags as $index => $tag) {
|
||||||
|
$alias = 't_' . $index;
|
||||||
|
$qb->join('s.tags', $alias, Join::WITH, $alias . '.name = :tag' . $index)
|
||||||
|
->setParameter('tag' . $index, $tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\ShortUrl\Repository;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering;
|
||||||
|
|
||||||
|
interface ShortUrlListRepositoryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return ShortUrl[]
|
||||||
|
*/
|
||||||
|
public function findList(ShortUrlsListFiltering $filtering): array;
|
||||||
|
|
||||||
|
public function countList(ShortUrlsCountFiltering $filtering): int;
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\ShortUrl\Repository;
|
namespace Shlinkio\Shlink\Core\ShortUrl\Repository;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
|
||||||
use Doctrine\DBAL\LockMode;
|
use Doctrine\DBAL\LockMode;
|
||||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||||
use Doctrine\ORM\Query\Expr\Join;
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
|
@ -15,152 +14,12 @@ use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
|
||||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||||
|
|
||||||
use function array_column;
|
|
||||||
use function count;
|
use function count;
|
||||||
use function Functional\contains;
|
|
||||||
use function sprintf;
|
|
||||||
|
|
||||||
class ShortUrlRepository extends EntitySpecificationRepository implements ShortUrlRepositoryInterface
|
class ShortUrlRepository extends EntitySpecificationRepository implements ShortUrlRepositoryInterface
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @return ShortUrl[]
|
|
||||||
*/
|
|
||||||
public function findList(ShortUrlsListFiltering $filtering): array
|
|
||||||
{
|
|
||||||
$qb = $this->createListQueryBuilder($filtering);
|
|
||||||
$qb->select('DISTINCT s')
|
|
||||||
->setMaxResults($filtering->limit)
|
|
||||||
->setFirstResult($filtering->offset);
|
|
||||||
|
|
||||||
// In case the ordering has been specified, the query could be more complex. Process it
|
|
||||||
$this->processOrderByForList($qb, $filtering);
|
|
||||||
|
|
||||||
$result = $qb->getQuery()->getResult();
|
|
||||||
if ($filtering->orderBy->field === 'visits') {
|
|
||||||
return array_column($result, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function processOrderByForList(QueryBuilder $qb, ShortUrlsListFiltering $filtering): void
|
|
||||||
{
|
|
||||||
// With no explicit order by, fallback to dateCreated-DESC
|
|
||||||
if (! $filtering->orderBy->hasOrderField()) {
|
|
||||||
$qb->orderBy('s.dateCreated', 'DESC');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$fieldName = $filtering->orderBy->field;
|
|
||||||
$order = $filtering->orderBy->direction;
|
|
||||||
|
|
||||||
if ($fieldName === 'visits') {
|
|
||||||
// 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)')
|
|
||||||
->leftJoin('s.visits', 'v')
|
|
||||||
->groupBy('s')
|
|
||||||
->orderBy('COUNT(DISTINCT v)', $order);
|
|
||||||
} elseif (contains(['longUrl', 'shortCode', 'dateCreated', 'title'], $fieldName)) {
|
|
||||||
$qb->orderBy('s.' . $fieldName, $order);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function countList(ShortUrlsCountFiltering $filtering): int
|
|
||||||
{
|
|
||||||
$qb = $this->createListQueryBuilder($filtering);
|
|
||||||
$qb->select('COUNT(DISTINCT s)');
|
|
||||||
|
|
||||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function createListQueryBuilder(ShortUrlsCountFiltering $filtering): QueryBuilder
|
|
||||||
{
|
|
||||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
|
||||||
$qb->from(ShortUrl::class, 's')
|
|
||||||
->where('1=1');
|
|
||||||
|
|
||||||
$dateRange = $filtering->dateRange;
|
|
||||||
if ($dateRange?->startDate !== null) {
|
|
||||||
$qb->andWhere($qb->expr()->gte('s.dateCreated', ':startDate'));
|
|
||||||
$qb->setParameter('startDate', $dateRange->startDate, ChronosDateTimeType::CHRONOS_DATETIME);
|
|
||||||
}
|
|
||||||
if ($dateRange?->endDate !== null) {
|
|
||||||
$qb->andWhere($qb->expr()->lte('s.dateCreated', ':endDate'));
|
|
||||||
$qb->setParameter('endDate', $dateRange->endDate, ChronosDateTimeType::CHRONOS_DATETIME);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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
|
|
||||||
if (empty($tags)) {
|
|
||||||
$qb->leftJoin('s.tags', 't');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply general search conditions
|
|
||||||
$conditions = [
|
|
||||||
$qb->expr()->like('s.longUrl', ':searchPattern'),
|
|
||||||
$qb->expr()->like('s.shortCode', ':searchPattern'),
|
|
||||||
$qb->expr()->like('s.title', ':searchPattern'),
|
|
||||||
$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;
|
|
||||||
if (empty($tags) || $tagsMode === TagsMode::ANY) {
|
|
||||||
$conditions[] = $qb->expr()->like('t.name', ':searchPattern');
|
|
||||||
}
|
|
||||||
|
|
||||||
$qb->leftJoin('s.domain', 'd')
|
|
||||||
->andWhere($qb->expr()->orX(...$conditions))
|
|
||||||
->setParameter('searchPattern', '%' . $searchTerm . '%');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by tags if provided
|
|
||||||
if (! empty($tags)) {
|
|
||||||
$tagsMode = $filtering->tagsMode ?? TagsMode::ANY;
|
|
||||||
$tagsMode === TagsMode::ANY
|
|
||||||
? $qb->join('s.tags', 't')->andWhere($qb->expr()->in('t.name', $tags))
|
|
||||||
: $this->joinAllTags($qb, $tags);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($filtering->excludeMaxVisitsReached) {
|
|
||||||
$qb->andWhere($qb->expr()->orX(
|
|
||||||
$qb->expr()->isNull('s.maxVisits'),
|
|
||||||
$qb->expr()->gt(
|
|
||||||
's.maxVisits',
|
|
||||||
sprintf('(SELECT COUNT(innerV.id) FROM %s as innerV WHERE innerV.shortUrl=s)', Visit::class),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($filtering->excludePastValidUntil) {
|
|
||||||
$qb
|
|
||||||
->andWhere($qb->expr()->orX(
|
|
||||||
$qb->expr()->isNull('s.validUntil'),
|
|
||||||
$qb->expr()->gte('s.validUntil', ':minValidUntil'),
|
|
||||||
))
|
|
||||||
->setParameter('minValidUntil', Chronos::now()->toDateTimeString());
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->applySpecification($qb, $filtering->apiKey?->spec(), 's');
|
|
||||||
|
|
||||||
return $qb;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findOneWithDomainFallback(ShortUrlIdentifier $identifier): ?ShortUrl
|
public function findOneWithDomainFallback(ShortUrlIdentifier $identifier): ?ShortUrl
|
||||||
{
|
{
|
||||||
// When ordering DESC, Postgres puts nulls at the beginning while the rest of supported DB engines put them at
|
// When ordering DESC, Postgres puts nulls at the beginning while the rest of supported DB engines put them at
|
||||||
|
|
|
@ -10,16 +10,10 @@ use Happyr\DoctrineSpecification\Specification\Specification;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering;
|
|
||||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||||
|
|
||||||
interface ShortUrlRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface
|
interface ShortUrlRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface
|
||||||
{
|
{
|
||||||
public function findList(ShortUrlsListFiltering $filtering): array;
|
|
||||||
|
|
||||||
public function countList(ShortUrlsCountFiltering $filtering): int;
|
|
||||||
|
|
||||||
public function findOneWithDomainFallback(ShortUrlIdentifier $identifier): ?ShortUrl;
|
public function findOneWithDomainFallback(ShortUrlIdentifier $identifier): ?ShortUrl;
|
||||||
|
|
||||||
public function findOne(ShortUrlIdentifier $identifier, ?Specification $spec = null): ?ShortUrl;
|
public function findOne(ShortUrlIdentifier $identifier, ?Specification $spec = null): ?ShortUrl;
|
||||||
|
|
31
module/Core/src/ShortUrl/ShortUrlListService.php
Normal file
31
module/Core/src/ShortUrl/ShortUrlListService.php
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\ShortUrl;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Paginator\Adapter\ShortUrlRepositoryAdapter;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlListRepositoryInterface;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
|
class ShortUrlListService implements ShortUrlListServiceInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ShortUrlListRepositoryInterface $repo,
|
||||||
|
private readonly UrlShortenerOptions $urlShortenerOptions,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listShortUrls(ShortUrlsParams $params, ?ApiKey $apiKey = null): Paginator
|
||||||
|
{
|
||||||
|
$defaultDomain = $this->urlShortenerOptions->domain['hostname'] ?? '';
|
||||||
|
$paginator = new Paginator(new ShortUrlRepositoryAdapter($this->repo, $params, $apiKey, $defaultDomain));
|
||||||
|
$paginator->setMaxPerPage($params->itemsPerPage)
|
||||||
|
->setCurrentPage($params->page);
|
||||||
|
|
||||||
|
return $paginator;
|
||||||
|
}
|
||||||
|
}
|
18
module/Core/src/ShortUrl/ShortUrlListServiceInterface.php
Normal file
18
module/Core/src/ShortUrl/ShortUrlListServiceInterface.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\ShortUrl;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
|
interface ShortUrlListServiceInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return ShortUrl[]|Paginator
|
||||||
|
*/
|
||||||
|
public function listShortUrls(ShortUrlsParams $params, ?ApiKey $apiKey = null): Paginator;
|
||||||
|
}
|
|
@ -5,17 +5,12 @@ declare(strict_types=1);
|
||||||
namespace Shlinkio\Shlink\Core\ShortUrl;
|
namespace Shlinkio\Shlink\Core\ShortUrl;
|
||||||
|
|
||||||
use Doctrine\ORM;
|
use Doctrine\ORM;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Paginator\Adapter\ShortUrlRepositoryAdapter;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepository;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
|
@ -26,25 +21,9 @@ class ShortUrlService implements ShortUrlServiceInterface
|
||||||
private readonly ShortUrlResolverInterface $urlResolver,
|
private readonly ShortUrlResolverInterface $urlResolver,
|
||||||
private readonly ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
private readonly ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
||||||
private readonly ShortUrlRelationResolverInterface $relationResolver,
|
private readonly ShortUrlRelationResolverInterface $relationResolver,
|
||||||
private readonly UrlShortenerOptions $urlShortenerOptions,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return ShortUrl[]|Paginator
|
|
||||||
*/
|
|
||||||
public function listShortUrls(ShortUrlsParams $params, ?ApiKey $apiKey = null): Paginator
|
|
||||||
{
|
|
||||||
/** @var ShortUrlRepository $repo */
|
|
||||||
$repo = $this->em->getRepository(ShortUrl::class);
|
|
||||||
$defaultDomain = $this->urlShortenerOptions->domain['hostname'] ?? '';
|
|
||||||
$paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $params, $apiKey, $defaultDomain));
|
|
||||||
$paginator->setMaxPerPage($params->itemsPerPage)
|
|
||||||
->setCurrentPage($params->page);
|
|
||||||
|
|
||||||
return $paginator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws ShortUrlNotFoundException
|
* @throws ShortUrlNotFoundException
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
|
|
|
@ -4,22 +4,15 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\ShortUrl;
|
namespace Shlinkio\Shlink\Core\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
interface ShortUrlServiceInterface
|
interface ShortUrlServiceInterface
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @return ShortUrl[]|Paginator
|
|
||||||
*/
|
|
||||||
public function listShortUrls(ShortUrlsParams $params, ?ApiKey $apiKey = null): Paginator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws ShortUrlNotFoundException
|
* @throws ShortUrlNotFoundException
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
|
|
|
@ -0,0 +1,337 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioDbTest\Shlink\Core\ShortUrl\Repository;
|
||||||
|
|
||||||
|
use Cake\Chronos\Chronos;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use ReflectionObject;
|
||||||
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Ordering;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlListRepository;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||||
|
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
||||||
|
|
||||||
|
use function count;
|
||||||
|
|
||||||
|
class ShortUrlListRepositoryTest extends DatabaseTestCase
|
||||||
|
{
|
||||||
|
private ShortUrlListRepository $repo;
|
||||||
|
private PersistenceShortUrlRelationResolver $relationResolver;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$this->repo = new ShortUrlListRepository($em, $em->getClassMetadata(ShortUrl::class));
|
||||||
|
$this->relationResolver = new PersistenceShortUrlRelationResolver($em);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function countListReturnsProperNumberOfResults(): void
|
||||||
|
{
|
||||||
|
$count = 5;
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$this->getEntityManager()->persist(ShortUrl::withLongUrl((string) $i));
|
||||||
|
}
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
|
self::assertEquals($count, $this->repo->countList(new ShortUrlsCountFiltering()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function findListProperlyFiltersResult(): void
|
||||||
|
{
|
||||||
|
$foo = ShortUrl::create(
|
||||||
|
ShortUrlCreation::fromRawData(['longUrl' => 'foo', 'tags' => ['bar']]),
|
||||||
|
$this->relationResolver,
|
||||||
|
);
|
||||||
|
$this->getEntityManager()->persist($foo);
|
||||||
|
|
||||||
|
$bar = ShortUrl::withLongUrl('bar');
|
||||||
|
$visit = Visit::forValidShortUrl($bar, Visitor::emptyInstance());
|
||||||
|
$this->getEntityManager()->persist($visit);
|
||||||
|
$bar->setVisits(new ArrayCollection([$visit]));
|
||||||
|
$this->getEntityManager()->persist($bar);
|
||||||
|
|
||||||
|
$foo2 = ShortUrl::withLongUrl('foo_2');
|
||||||
|
$ref = new ReflectionObject($foo2);
|
||||||
|
$dateProp = $ref->getProperty('dateCreated');
|
||||||
|
$dateProp->setAccessible(true);
|
||||||
|
$dateProp->setValue($foo2, Chronos::now()->subDays(5));
|
||||||
|
$this->getEntityManager()->persist($foo2);
|
||||||
|
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
|
$result = $this->repo->findList(
|
||||||
|
new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), 'foo', ['bar']),
|
||||||
|
);
|
||||||
|
self::assertCount(1, $result);
|
||||||
|
self::assertEquals(1, $this->repo->countList(new ShortUrlsCountFiltering('foo', ['bar'])));
|
||||||
|
self::assertSame($foo, $result[0]);
|
||||||
|
|
||||||
|
// Assert searched text also applies to tags
|
||||||
|
$result = $this->repo->findList(new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), 'bar'));
|
||||||
|
self::assertCount(2, $result);
|
||||||
|
self::assertEquals(2, $this->repo->countList(new ShortUrlsCountFiltering('bar')));
|
||||||
|
self::assertContains($foo, $result);
|
||||||
|
|
||||||
|
$result = $this->repo->findList(new ShortUrlsListFiltering(null, null, Ordering::emptyInstance()));
|
||||||
|
self::assertCount(3, $result);
|
||||||
|
|
||||||
|
$result = $this->repo->findList(new ShortUrlsListFiltering(2, null, Ordering::emptyInstance()));
|
||||||
|
self::assertCount(2, $result);
|
||||||
|
|
||||||
|
$result = $this->repo->findList(new ShortUrlsListFiltering(2, 1, Ordering::emptyInstance()));
|
||||||
|
self::assertCount(2, $result);
|
||||||
|
|
||||||
|
self::assertCount(1, $this->repo->findList(new ShortUrlsListFiltering(2, 2, Ordering::emptyInstance())));
|
||||||
|
|
||||||
|
$result = $this->repo->findList(
|
||||||
|
new ShortUrlsListFiltering(null, null, Ordering::fromTuple(['visits', 'DESC'])),
|
||||||
|
);
|
||||||
|
self::assertCount(3, $result);
|
||||||
|
self::assertSame($bar, $result[0]);
|
||||||
|
|
||||||
|
$result = $this->repo->findList(
|
||||||
|
new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), null, [], null, DateRange::until(
|
||||||
|
Chronos::now()->subDays(2),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
self::assertCount(1, $result);
|
||||||
|
self::assertEquals(1, $this->repo->countList(new ShortUrlsCountFiltering(null, [], null, DateRange::until(
|
||||||
|
Chronos::now()->subDays(2),
|
||||||
|
))));
|
||||||
|
self::assertSame($foo2, $result[0]);
|
||||||
|
|
||||||
|
self::assertCount(2, $this->repo->findList(
|
||||||
|
new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), null, [], null, DateRange::since(
|
||||||
|
Chronos::now()->subDays(2),
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
self::assertEquals(2, $this->repo->countList(
|
||||||
|
new ShortUrlsCountFiltering(null, [], null, DateRange::since(Chronos::now()->subDays(2))),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function findListProperlyMapsFieldNamesToColumnNamesWhenOrdering(): void
|
||||||
|
{
|
||||||
|
$urls = ['a', 'z', 'c', 'b'];
|
||||||
|
foreach ($urls as $url) {
|
||||||
|
$this->getEntityManager()->persist(ShortUrl::withLongUrl($url));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
|
$result = $this->repo->findList(
|
||||||
|
new ShortUrlsListFiltering(null, null, Ordering::fromTuple(['longUrl', 'ASC'])),
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertCount(count($urls), $result);
|
||||||
|
self::assertEquals('a', $result[0]->getLongUrl());
|
||||||
|
self::assertEquals('b', $result[1]->getLongUrl());
|
||||||
|
self::assertEquals('c', $result[2]->getLongUrl());
|
||||||
|
self::assertEquals('z', $result[3]->getLongUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function findListReturnsOnlyThoseWithMatchingTags(): void
|
||||||
|
{
|
||||||
|
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
|
'longUrl' => 'foo1',
|
||||||
|
'tags' => ['foo', 'bar'],
|
||||||
|
]), $this->relationResolver);
|
||||||
|
$this->getEntityManager()->persist($shortUrl1);
|
||||||
|
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
|
'longUrl' => 'foo2',
|
||||||
|
'tags' => ['foo', 'baz'],
|
||||||
|
]), $this->relationResolver);
|
||||||
|
$this->getEntityManager()->persist($shortUrl2);
|
||||||
|
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
|
'longUrl' => 'foo3',
|
||||||
|
'tags' => ['foo'],
|
||||||
|
]), $this->relationResolver);
|
||||||
|
$this->getEntityManager()->persist($shortUrl3);
|
||||||
|
$shortUrl4 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
|
'longUrl' => 'foo4',
|
||||||
|
'tags' => ['bar', 'baz'],
|
||||||
|
]), $this->relationResolver);
|
||||||
|
$this->getEntityManager()->persist($shortUrl4);
|
||||||
|
$shortUrl5 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
|
'longUrl' => 'foo5',
|
||||||
|
'tags' => ['bar', 'baz'],
|
||||||
|
]), $this->relationResolver);
|
||||||
|
$this->getEntityManager()->persist($shortUrl5);
|
||||||
|
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
|
self::assertCount(5, $this->repo->findList(
|
||||||
|
new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), null, ['foo', 'bar']),
|
||||||
|
));
|
||||||
|
self::assertCount(5, $this->repo->findList(new ShortUrlsListFiltering(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Ordering::emptyInstance(),
|
||||||
|
null,
|
||||||
|
['foo', 'bar'],
|
||||||
|
TagsMode::ANY,
|
||||||
|
)));
|
||||||
|
self::assertCount(1, $this->repo->findList(new ShortUrlsListFiltering(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Ordering::emptyInstance(),
|
||||||
|
null,
|
||||||
|
['foo', 'bar'],
|
||||||
|
TagsMode::ALL,
|
||||||
|
)));
|
||||||
|
self::assertEquals(5, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar'])));
|
||||||
|
self::assertEquals(5, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar'], TagsMode::ANY)));
|
||||||
|
self::assertEquals(1, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar'], TagsMode::ALL)));
|
||||||
|
|
||||||
|
self::assertCount(4, $this->repo->findList(
|
||||||
|
new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), null, ['bar', 'baz']),
|
||||||
|
));
|
||||||
|
self::assertCount(4, $this->repo->findList(new ShortUrlsListFiltering(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Ordering::emptyInstance(),
|
||||||
|
null,
|
||||||
|
['bar', 'baz'],
|
||||||
|
TagsMode::ANY,
|
||||||
|
)));
|
||||||
|
self::assertCount(2, $this->repo->findList(new ShortUrlsListFiltering(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Ordering::emptyInstance(),
|
||||||
|
null,
|
||||||
|
['bar', 'baz'],
|
||||||
|
TagsMode::ALL,
|
||||||
|
)));
|
||||||
|
self::assertEquals(4, $this->repo->countList(new ShortUrlsCountFiltering(null, ['bar', 'baz'])));
|
||||||
|
self::assertEquals(4, $this->repo->countList(
|
||||||
|
new ShortUrlsCountFiltering(null, ['bar', 'baz'], TagsMode::ANY),
|
||||||
|
));
|
||||||
|
self::assertEquals(2, $this->repo->countList(
|
||||||
|
new ShortUrlsCountFiltering(null, ['bar', 'baz'], TagsMode::ALL),
|
||||||
|
));
|
||||||
|
|
||||||
|
self::assertCount(5, $this->repo->findList(
|
||||||
|
new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), null, ['foo', 'bar', 'baz']),
|
||||||
|
));
|
||||||
|
self::assertCount(5, $this->repo->findList(new ShortUrlsListFiltering(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Ordering::emptyInstance(),
|
||||||
|
null,
|
||||||
|
['foo', 'bar', 'baz'],
|
||||||
|
TagsMode::ANY,
|
||||||
|
)));
|
||||||
|
self::assertCount(0, $this->repo->findList(new ShortUrlsListFiltering(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Ordering::emptyInstance(),
|
||||||
|
null,
|
||||||
|
['foo', 'bar', 'baz'],
|
||||||
|
TagsMode::ALL,
|
||||||
|
)));
|
||||||
|
self::assertEquals(5, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'])));
|
||||||
|
self::assertEquals(5, $this->repo->countList(
|
||||||
|
new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'], TagsMode::ANY),
|
||||||
|
));
|
||||||
|
self::assertEquals(0, $this->repo->countList(
|
||||||
|
new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'], TagsMode::ALL),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function findListReturnsOnlyThoseWithMatchingDomains(): void
|
||||||
|
{
|
||||||
|
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
|
'longUrl' => 'foo1',
|
||||||
|
'domain' => null,
|
||||||
|
]), $this->relationResolver);
|
||||||
|
$this->getEntityManager()->persist($shortUrl1);
|
||||||
|
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
|
'longUrl' => 'foo2',
|
||||||
|
'domain' => null,
|
||||||
|
]), $this->relationResolver);
|
||||||
|
$this->getEntityManager()->persist($shortUrl2);
|
||||||
|
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
|
'longUrl' => 'foo3',
|
||||||
|
'domain' => 'another.com',
|
||||||
|
]), $this->relationResolver);
|
||||||
|
$this->getEntityManager()->persist($shortUrl3);
|
||||||
|
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
|
$buildFiltering = static fn (string $searchTerm) => new ShortUrlsListFiltering(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Ordering::emptyInstance(),
|
||||||
|
searchTerm: $searchTerm,
|
||||||
|
defaultDomain: 'deFaulT-domain.com',
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertCount(2, $this->repo->findList($buildFiltering('default-dom')));
|
||||||
|
self::assertCount(2, $this->repo->findList($buildFiltering('DOM')));
|
||||||
|
self::assertCount(1, $this->repo->findList($buildFiltering('another')));
|
||||||
|
self::assertCount(3, $this->repo->findList($buildFiltering('foo')));
|
||||||
|
self::assertCount(0, $this->repo->findList($buildFiltering('no results')));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function findListReturnsOnlyThoseWithoutExcludedUrls(): void
|
||||||
|
{
|
||||||
|
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
|
'longUrl' => 'foo1',
|
||||||
|
'validUntil' => Chronos::now()->addDays(1)->toAtomString(),
|
||||||
|
'maxVisits' => 100,
|
||||||
|
]), $this->relationResolver);
|
||||||
|
$this->getEntityManager()->persist($shortUrl1);
|
||||||
|
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
|
'longUrl' => 'foo2',
|
||||||
|
'validUntil' => Chronos::now()->subDays(1)->toAtomString(),
|
||||||
|
]), $this->relationResolver);
|
||||||
|
$this->getEntityManager()->persist($shortUrl2);
|
||||||
|
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
|
'longUrl' => 'foo3',
|
||||||
|
]), $this->relationResolver);
|
||||||
|
$this->getEntityManager()->persist($shortUrl3);
|
||||||
|
$shortUrl4 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
|
'longUrl' => 'foo4',
|
||||||
|
'maxVisits' => 3,
|
||||||
|
]), $this->relationResolver);
|
||||||
|
$this->getEntityManager()->persist($shortUrl4);
|
||||||
|
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::emptyInstance()));
|
||||||
|
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::emptyInstance()));
|
||||||
|
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::emptyInstance()));
|
||||||
|
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
|
$filtering = static fn (bool $excludeMaxVisitsReached, bool $excludePastValidUntil) =>
|
||||||
|
new ShortUrlsListFiltering(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Ordering::emptyInstance(),
|
||||||
|
excludeMaxVisitsReached: $excludeMaxVisitsReached,
|
||||||
|
excludePastValidUntil: $excludePastValidUntil,
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertCount(4, $this->repo->findList($filtering(false, false)));
|
||||||
|
self::assertEquals(4, $this->repo->countList($filtering(false, false)));
|
||||||
|
self::assertCount(3, $this->repo->findList($filtering(true, false)));
|
||||||
|
self::assertEquals(3, $this->repo->countList($filtering(true, false)));
|
||||||
|
self::assertCount(3, $this->repo->findList($filtering(false, true)));
|
||||||
|
self::assertEquals(3, $this->repo->countList($filtering(false, true)));
|
||||||
|
self::assertCount(2, $this->repo->findList($filtering(true, true)));
|
||||||
|
self::assertEquals(2, $this->repo->countList($filtering(true, true)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,21 +5,12 @@ declare(strict_types=1);
|
||||||
namespace ShlinkioDbTest\Shlink\Core\ShortUrl\Repository;
|
namespace ShlinkioDbTest\Shlink\Core\ShortUrl\Repository;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
|
||||||
use ReflectionObject;
|
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
|
||||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||||
use Shlinkio\Shlink\Core\Model\Ordering;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepository;
|
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepository;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver;
|
use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver;
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
|
||||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||||
use Shlinkio\Shlink\Importer\Sources\ImportSource;
|
use Shlinkio\Shlink\Importer\Sources\ImportSource;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
||||||
|
@ -27,8 +18,6 @@ use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
||||||
|
|
||||||
use function count;
|
|
||||||
|
|
||||||
class ShortUrlRepositoryTest extends DatabaseTestCase
|
class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||||
{
|
{
|
||||||
private ShortUrlRepository $repo;
|
private ShortUrlRepository $repo;
|
||||||
|
@ -86,307 +75,6 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
|
||||||
public function countListReturnsProperNumberOfResults(): void
|
|
||||||
{
|
|
||||||
$count = 5;
|
|
||||||
for ($i = 0; $i < $count; $i++) {
|
|
||||||
$this->getEntityManager()->persist(ShortUrl::withLongUrl((string) $i));
|
|
||||||
}
|
|
||||||
$this->getEntityManager()->flush();
|
|
||||||
|
|
||||||
self::assertEquals($count, $this->repo->countList(new ShortUrlsCountFiltering()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @test */
|
|
||||||
public function findListProperlyFiltersResult(): void
|
|
||||||
{
|
|
||||||
$foo = ShortUrl::create(
|
|
||||||
ShortUrlCreation::fromRawData(['longUrl' => 'foo', 'tags' => ['bar']]),
|
|
||||||
$this->relationResolver,
|
|
||||||
);
|
|
||||||
$this->getEntityManager()->persist($foo);
|
|
||||||
|
|
||||||
$bar = ShortUrl::withLongUrl('bar');
|
|
||||||
$visit = Visit::forValidShortUrl($bar, Visitor::emptyInstance());
|
|
||||||
$this->getEntityManager()->persist($visit);
|
|
||||||
$bar->setVisits(new ArrayCollection([$visit]));
|
|
||||||
$this->getEntityManager()->persist($bar);
|
|
||||||
|
|
||||||
$foo2 = ShortUrl::withLongUrl('foo_2');
|
|
||||||
$ref = new ReflectionObject($foo2);
|
|
||||||
$dateProp = $ref->getProperty('dateCreated');
|
|
||||||
$dateProp->setAccessible(true);
|
|
||||||
$dateProp->setValue($foo2, Chronos::now()->subDays(5));
|
|
||||||
$this->getEntityManager()->persist($foo2);
|
|
||||||
|
|
||||||
$this->getEntityManager()->flush();
|
|
||||||
|
|
||||||
$result = $this->repo->findList(
|
|
||||||
new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), 'foo', ['bar']),
|
|
||||||
);
|
|
||||||
self::assertCount(1, $result);
|
|
||||||
self::assertEquals(1, $this->repo->countList(new ShortUrlsCountFiltering('foo', ['bar'])));
|
|
||||||
self::assertSame($foo, $result[0]);
|
|
||||||
|
|
||||||
// Assert searched text also applies to tags
|
|
||||||
$result = $this->repo->findList(new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), 'bar'));
|
|
||||||
self::assertCount(2, $result);
|
|
||||||
self::assertEquals(2, $this->repo->countList(new ShortUrlsCountFiltering('bar')));
|
|
||||||
self::assertContains($foo, $result);
|
|
||||||
|
|
||||||
$result = $this->repo->findList(new ShortUrlsListFiltering(null, null, Ordering::emptyInstance()));
|
|
||||||
self::assertCount(3, $result);
|
|
||||||
|
|
||||||
$result = $this->repo->findList(new ShortUrlsListFiltering(2, null, Ordering::emptyInstance()));
|
|
||||||
self::assertCount(2, $result);
|
|
||||||
|
|
||||||
$result = $this->repo->findList(new ShortUrlsListFiltering(2, 1, Ordering::emptyInstance()));
|
|
||||||
self::assertCount(2, $result);
|
|
||||||
|
|
||||||
self::assertCount(1, $this->repo->findList(new ShortUrlsListFiltering(2, 2, Ordering::emptyInstance())));
|
|
||||||
|
|
||||||
$result = $this->repo->findList(
|
|
||||||
new ShortUrlsListFiltering(null, null, Ordering::fromTuple(['visits', 'DESC'])),
|
|
||||||
);
|
|
||||||
self::assertCount(3, $result);
|
|
||||||
self::assertSame($bar, $result[0]);
|
|
||||||
|
|
||||||
$result = $this->repo->findList(
|
|
||||||
new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), null, [], null, DateRange::until(
|
|
||||||
Chronos::now()->subDays(2),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
self::assertCount(1, $result);
|
|
||||||
self::assertEquals(1, $this->repo->countList(new ShortUrlsCountFiltering(null, [], null, DateRange::until(
|
|
||||||
Chronos::now()->subDays(2),
|
|
||||||
))));
|
|
||||||
self::assertSame($foo2, $result[0]);
|
|
||||||
|
|
||||||
self::assertCount(2, $this->repo->findList(
|
|
||||||
new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), null, [], null, DateRange::since(
|
|
||||||
Chronos::now()->subDays(2),
|
|
||||||
)),
|
|
||||||
));
|
|
||||||
self::assertEquals(2, $this->repo->countList(
|
|
||||||
new ShortUrlsCountFiltering(null, [], null, DateRange::since(Chronos::now()->subDays(2))),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @test */
|
|
||||||
public function findListProperlyMapsFieldNamesToColumnNamesWhenOrdering(): void
|
|
||||||
{
|
|
||||||
$urls = ['a', 'z', 'c', 'b'];
|
|
||||||
foreach ($urls as $url) {
|
|
||||||
$this->getEntityManager()->persist(ShortUrl::withLongUrl($url));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->getEntityManager()->flush();
|
|
||||||
|
|
||||||
$result = $this->repo->findList(
|
|
||||||
new ShortUrlsListFiltering(null, null, Ordering::fromTuple(['longUrl', 'ASC'])),
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertCount(count($urls), $result);
|
|
||||||
self::assertEquals('a', $result[0]->getLongUrl());
|
|
||||||
self::assertEquals('b', $result[1]->getLongUrl());
|
|
||||||
self::assertEquals('c', $result[2]->getLongUrl());
|
|
||||||
self::assertEquals('z', $result[3]->getLongUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @test */
|
|
||||||
public function findListReturnsOnlyThoseWithMatchingTags(): void
|
|
||||||
{
|
|
||||||
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
|
||||||
'longUrl' => 'foo1',
|
|
||||||
'tags' => ['foo', 'bar'],
|
|
||||||
]), $this->relationResolver);
|
|
||||||
$this->getEntityManager()->persist($shortUrl1);
|
|
||||||
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
|
||||||
'longUrl' => 'foo2',
|
|
||||||
'tags' => ['foo', 'baz'],
|
|
||||||
]), $this->relationResolver);
|
|
||||||
$this->getEntityManager()->persist($shortUrl2);
|
|
||||||
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
|
||||||
'longUrl' => 'foo3',
|
|
||||||
'tags' => ['foo'],
|
|
||||||
]), $this->relationResolver);
|
|
||||||
$this->getEntityManager()->persist($shortUrl3);
|
|
||||||
$shortUrl4 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
|
||||||
'longUrl' => 'foo4',
|
|
||||||
'tags' => ['bar', 'baz'],
|
|
||||||
]), $this->relationResolver);
|
|
||||||
$this->getEntityManager()->persist($shortUrl4);
|
|
||||||
$shortUrl5 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
|
||||||
'longUrl' => 'foo5',
|
|
||||||
'tags' => ['bar', 'baz'],
|
|
||||||
]), $this->relationResolver);
|
|
||||||
$this->getEntityManager()->persist($shortUrl5);
|
|
||||||
|
|
||||||
$this->getEntityManager()->flush();
|
|
||||||
|
|
||||||
self::assertCount(5, $this->repo->findList(
|
|
||||||
new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), null, ['foo', 'bar']),
|
|
||||||
));
|
|
||||||
self::assertCount(5, $this->repo->findList(new ShortUrlsListFiltering(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
Ordering::emptyInstance(),
|
|
||||||
null,
|
|
||||||
['foo', 'bar'],
|
|
||||||
TagsMode::ANY,
|
|
||||||
)));
|
|
||||||
self::assertCount(1, $this->repo->findList(new ShortUrlsListFiltering(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
Ordering::emptyInstance(),
|
|
||||||
null,
|
|
||||||
['foo', 'bar'],
|
|
||||||
TagsMode::ALL,
|
|
||||||
)));
|
|
||||||
self::assertEquals(5, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar'])));
|
|
||||||
self::assertEquals(5, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar'], TagsMode::ANY)));
|
|
||||||
self::assertEquals(1, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar'], TagsMode::ALL)));
|
|
||||||
|
|
||||||
self::assertCount(4, $this->repo->findList(
|
|
||||||
new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), null, ['bar', 'baz']),
|
|
||||||
));
|
|
||||||
self::assertCount(4, $this->repo->findList(new ShortUrlsListFiltering(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
Ordering::emptyInstance(),
|
|
||||||
null,
|
|
||||||
['bar', 'baz'],
|
|
||||||
TagsMode::ANY,
|
|
||||||
)));
|
|
||||||
self::assertCount(2, $this->repo->findList(new ShortUrlsListFiltering(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
Ordering::emptyInstance(),
|
|
||||||
null,
|
|
||||||
['bar', 'baz'],
|
|
||||||
TagsMode::ALL,
|
|
||||||
)));
|
|
||||||
self::assertEquals(4, $this->repo->countList(new ShortUrlsCountFiltering(null, ['bar', 'baz'])));
|
|
||||||
self::assertEquals(4, $this->repo->countList(
|
|
||||||
new ShortUrlsCountFiltering(null, ['bar', 'baz'], TagsMode::ANY),
|
|
||||||
));
|
|
||||||
self::assertEquals(2, $this->repo->countList(
|
|
||||||
new ShortUrlsCountFiltering(null, ['bar', 'baz'], TagsMode::ALL),
|
|
||||||
));
|
|
||||||
|
|
||||||
self::assertCount(5, $this->repo->findList(
|
|
||||||
new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), null, ['foo', 'bar', 'baz']),
|
|
||||||
));
|
|
||||||
self::assertCount(5, $this->repo->findList(new ShortUrlsListFiltering(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
Ordering::emptyInstance(),
|
|
||||||
null,
|
|
||||||
['foo', 'bar', 'baz'],
|
|
||||||
TagsMode::ANY,
|
|
||||||
)));
|
|
||||||
self::assertCount(0, $this->repo->findList(new ShortUrlsListFiltering(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
Ordering::emptyInstance(),
|
|
||||||
null,
|
|
||||||
['foo', 'bar', 'baz'],
|
|
||||||
TagsMode::ALL,
|
|
||||||
)));
|
|
||||||
self::assertEquals(5, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'])));
|
|
||||||
self::assertEquals(5, $this->repo->countList(
|
|
||||||
new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'], TagsMode::ANY),
|
|
||||||
));
|
|
||||||
self::assertEquals(0, $this->repo->countList(
|
|
||||||
new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'], TagsMode::ALL),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @test */
|
|
||||||
public function findListReturnsOnlyThoseWithMatchingDomains(): void
|
|
||||||
{
|
|
||||||
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
|
||||||
'longUrl' => 'foo1',
|
|
||||||
'domain' => null,
|
|
||||||
]), $this->relationResolver);
|
|
||||||
$this->getEntityManager()->persist($shortUrl1);
|
|
||||||
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
|
||||||
'longUrl' => 'foo2',
|
|
||||||
'domain' => null,
|
|
||||||
]), $this->relationResolver);
|
|
||||||
$this->getEntityManager()->persist($shortUrl2);
|
|
||||||
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
|
||||||
'longUrl' => 'foo3',
|
|
||||||
'domain' => 'another.com',
|
|
||||||
]), $this->relationResolver);
|
|
||||||
$this->getEntityManager()->persist($shortUrl3);
|
|
||||||
|
|
||||||
$this->getEntityManager()->flush();
|
|
||||||
|
|
||||||
$buildFiltering = static fn (string $searchTerm) => new ShortUrlsListFiltering(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
Ordering::emptyInstance(),
|
|
||||||
searchTerm: $searchTerm,
|
|
||||||
defaultDomain: 'deFaulT-domain.com',
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertCount(2, $this->repo->findList($buildFiltering('default-dom')));
|
|
||||||
self::assertCount(2, $this->repo->findList($buildFiltering('DOM')));
|
|
||||||
self::assertCount(1, $this->repo->findList($buildFiltering('another')));
|
|
||||||
self::assertCount(3, $this->repo->findList($buildFiltering('foo')));
|
|
||||||
self::assertCount(0, $this->repo->findList($buildFiltering('no results')));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @test */
|
|
||||||
public function findListReturnsOnlyThoseWithoutExcludedUrls(): void
|
|
||||||
{
|
|
||||||
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
|
||||||
'longUrl' => 'foo1',
|
|
||||||
'validUntil' => Chronos::now()->addDays(1)->toAtomString(),
|
|
||||||
'maxVisits' => 100,
|
|
||||||
]), $this->relationResolver);
|
|
||||||
$this->getEntityManager()->persist($shortUrl1);
|
|
||||||
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
|
||||||
'longUrl' => 'foo2',
|
|
||||||
'validUntil' => Chronos::now()->subDays(1)->toAtomString(),
|
|
||||||
]), $this->relationResolver);
|
|
||||||
$this->getEntityManager()->persist($shortUrl2);
|
|
||||||
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
|
||||||
'longUrl' => 'foo3',
|
|
||||||
]), $this->relationResolver);
|
|
||||||
$this->getEntityManager()->persist($shortUrl3);
|
|
||||||
$shortUrl4 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
|
||||||
'longUrl' => 'foo4',
|
|
||||||
'maxVisits' => 3,
|
|
||||||
]), $this->relationResolver);
|
|
||||||
$this->getEntityManager()->persist($shortUrl4);
|
|
||||||
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::emptyInstance()));
|
|
||||||
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::emptyInstance()));
|
|
||||||
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::emptyInstance()));
|
|
||||||
|
|
||||||
$this->getEntityManager()->flush();
|
|
||||||
|
|
||||||
$filtering = static fn (bool $excludeMaxVisitsReached, bool $excludePastValidUntil) =>
|
|
||||||
new ShortUrlsListFiltering(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
Ordering::emptyInstance(),
|
|
||||||
excludeMaxVisitsReached: $excludeMaxVisitsReached,
|
|
||||||
excludePastValidUntil: $excludePastValidUntil,
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertCount(4, $this->repo->findList($filtering(false, false)));
|
|
||||||
self::assertEquals(4, $this->repo->countList($filtering(false, false)));
|
|
||||||
self::assertCount(3, $this->repo->findList($filtering(true, false)));
|
|
||||||
self::assertEquals(3, $this->repo->countList($filtering(true, false)));
|
|
||||||
self::assertCount(3, $this->repo->findList($filtering(false, true)));
|
|
||||||
self::assertEquals(3, $this->repo->countList($filtering(false, true)));
|
|
||||||
self::assertCount(2, $this->repo->findList($filtering(true, true)));
|
|
||||||
self::assertEquals(2, $this->repo->countList($filtering(true, true)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function shortCodeIsInUseLooksForShortUrlInProperSetOfTables(): void
|
public function shortCodeIsInUseLooksForShortUrlInProperSetOfTables(): void
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,16 +12,16 @@ use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Paginator\Adapter\ShortUrlRepositoryAdapter;
|
use Shlinkio\Shlink\Core\ShortUrl\Paginator\Adapter\ShortUrlRepositoryAdapter;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering;
|
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering;
|
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepositoryInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlListRepositoryInterface;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
class ShortUrlRepositoryAdapterTest extends TestCase
|
class ShortUrlRepositoryAdapterTest extends TestCase
|
||||||
{
|
{
|
||||||
private MockObject & ShortUrlRepositoryInterface $repo;
|
private MockObject & ShortUrlListRepositoryInterface $repo;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->repo = $this->createMock(ShortUrlRepositoryInterface::class);
|
$this->repo = $this->createMock(ShortUrlListRepositoryInterface::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
53
module/Core/test/ShortUrl/ShortUrlListServiceTest.php
Normal file
53
module/Core/test/ShortUrl/ShortUrlListServiceTest.php
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\Core\ShortUrl;
|
||||||
|
|
||||||
|
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\Model\ShortUrlsParams;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlListRepositoryInterface;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListService;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
use ShlinkioTest\Shlink\Core\Util\ApiKeyHelpersTrait;
|
||||||
|
|
||||||
|
use function count;
|
||||||
|
|
||||||
|
class ShortUrlListServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
use ApiKeyHelpersTrait;
|
||||||
|
|
||||||
|
private ShortUrlListService $service;
|
||||||
|
private MockObject & ShortUrlListRepositoryInterface $repo;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->repo = $this->createMock(ShortUrlListRepositoryInterface::class);
|
||||||
|
$this->service = new ShortUrlListService($this->repo, new UrlShortenerOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider provideAdminApiKeys
|
||||||
|
*/
|
||||||
|
public function listedUrlsAreReturnedFromEntityManager(?ApiKey $apiKey): void
|
||||||
|
{
|
||||||
|
$list = [
|
||||||
|
ShortUrl::createEmpty(),
|
||||||
|
ShortUrl::createEmpty(),
|
||||||
|
ShortUrl::createEmpty(),
|
||||||
|
ShortUrl::createEmpty(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->repo->expects($this->once())->method('findList')->willReturn($list);
|
||||||
|
$this->repo->expects($this->once())->method('countList')->willReturn(count($list));
|
||||||
|
|
||||||
|
$paginator = $this->service->listShortUrls(ShortUrlsParams::emptyInstance(), $apiKey);
|
||||||
|
|
||||||
|
self::assertCount(4, $paginator);
|
||||||
|
self::assertCount(4, $paginator->getCurrentPageResults());
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,21 +8,16 @@ use Cake\Chronos\Chronos;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepository;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\SimpleShortUrlRelationResolver;
|
use Shlinkio\Shlink\Core\ShortUrl\Resolver\SimpleShortUrlRelationResolver;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlService;
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlService;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
use ShlinkioTest\Shlink\Core\Util\ApiKeyHelpersTrait;
|
use ShlinkioTest\Shlink\Core\Util\ApiKeyHelpersTrait;
|
||||||
|
|
||||||
use function count;
|
|
||||||
|
|
||||||
class ShortUrlServiceTest extends TestCase
|
class ShortUrlServiceTest extends TestCase
|
||||||
{
|
{
|
||||||
use ApiKeyHelpersTrait;
|
use ApiKeyHelpersTrait;
|
||||||
|
@ -46,34 +41,9 @@ class ShortUrlServiceTest extends TestCase
|
||||||
$this->urlResolver,
|
$this->urlResolver,
|
||||||
$this->titleResolutionHelper,
|
$this->titleResolutionHelper,
|
||||||
new SimpleShortUrlRelationResolver(),
|
new SimpleShortUrlRelationResolver(),
|
||||||
new UrlShortenerOptions(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
* @dataProvider provideAdminApiKeys
|
|
||||||
*/
|
|
||||||
public function listedUrlsAreReturnedFromEntityManager(?ApiKey $apiKey): void
|
|
||||||
{
|
|
||||||
$list = [
|
|
||||||
ShortUrl::createEmpty(),
|
|
||||||
ShortUrl::createEmpty(),
|
|
||||||
ShortUrl::createEmpty(),
|
|
||||||
ShortUrl::createEmpty(),
|
|
||||||
];
|
|
||||||
|
|
||||||
$repo = $this->createMock(ShortUrlRepository::class);
|
|
||||||
$repo->expects($this->once())->method('findList')->willReturn($list);
|
|
||||||
$repo->expects($this->once())->method('countList')->willReturn(count($list));
|
|
||||||
$this->em->method('getRepository')->with(ShortUrl::class)->willReturn($repo);
|
|
||||||
|
|
||||||
$paginator = $this->service->listShortUrls(ShortUrlsParams::emptyInstance(), $apiKey);
|
|
||||||
|
|
||||||
self::assertCount(4, $paginator);
|
|
||||||
self::assertCount(4, $paginator->getCurrentPageResults());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
* @dataProvider provideShortUrlEdits
|
* @dataProvider provideShortUrlEdits
|
||||||
|
|
|
@ -90,7 +90,10 @@ return [
|
||||||
Visit\Transformer\OrphanVisitDataTransformer::class,
|
Visit\Transformer\OrphanVisitDataTransformer::class,
|
||||||
],
|
],
|
||||||
Action\Visit\NonOrphanVisitsAction::class => [Visit\VisitsStatsHelper::class],
|
Action\Visit\NonOrphanVisitsAction::class => [Visit\VisitsStatsHelper::class],
|
||||||
Action\ShortUrl\ListShortUrlsAction::class => [ShortUrl\ShortUrlService::class, ShortUrlDataTransformer::class],
|
Action\ShortUrl\ListShortUrlsAction::class => [
|
||||||
|
ShortUrl\ShortUrlListService::class,
|
||||||
|
ShortUrlDataTransformer::class,
|
||||||
|
],
|
||||||
Action\Tag\ListTagsAction::class => [TagService::class],
|
Action\Tag\ListTagsAction::class => [TagService::class],
|
||||||
Action\Tag\TagsStatsAction::class => [TagService::class],
|
Action\Tag\TagsStatsAction::class => [TagService::class],
|
||||||
Action\Tag\DeleteTagsAction::class => [TagService::class],
|
Action\Tag\DeleteTagsAction::class => [TagService::class],
|
||||||
|
|
|
@ -10,7 +10,7 @@ use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
|
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
|
||||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlServiceInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
|
||||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||||
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
|
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
|
||||||
|
|
||||||
|
@ -22,8 +22,8 @@ class ListShortUrlsAction extends AbstractRestAction
|
||||||
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
|
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ShortUrlServiceInterface $shortUrlService,
|
private readonly ShortUrlListServiceInterface $shortUrlService,
|
||||||
private DataTransformerInterface $transformer,
|
private readonly DataTransformerInterface $transformer,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlService;
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
||||||
use Shlinkio\Shlink\Rest\Action\ShortUrl\ListShortUrlsAction;
|
use Shlinkio\Shlink\Rest\Action\ShortUrl\ListShortUrlsAction;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
@ -21,11 +21,11 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
class ListShortUrlsActionTest extends TestCase
|
class ListShortUrlsActionTest extends TestCase
|
||||||
{
|
{
|
||||||
private ListShortUrlsAction $action;
|
private ListShortUrlsAction $action;
|
||||||
private MockObject & ShortUrlService $service;
|
private MockObject & ShortUrlListServiceInterface $service;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->service = $this->createMock(ShortUrlService::class);
|
$this->service = $this->createMock(ShortUrlListServiceInterface::class);
|
||||||
|
|
||||||
$this->action = new ListShortUrlsAction($this->service, new ShortUrlDataTransformer(
|
$this->action = new ListShortUrlsAction($this->service, new ShortUrlDataTransformer(
|
||||||
new ShortUrlStringifier([
|
new ShortUrlStringifier([
|
||||||
|
|
Loading…
Reference in a new issue