mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-26 04:09:02 +03:00
Create repository to handle expired short URLs deletion
This commit is contained in:
parent
f92a720d63
commit
fd882834d3
5 changed files with 226 additions and 0 deletions
|
@ -57,6 +57,10 @@ return [
|
||||||
EntityRepositoryFactory::class,
|
EntityRepositoryFactory::class,
|
||||||
ShortUrl\Entity\ShortUrl::class,
|
ShortUrl\Entity\ShortUrl::class,
|
||||||
],
|
],
|
||||||
|
ShortUrl\Repository\ExpiredShortUrlsRepository::class => [
|
||||||
|
EntityRepositoryFactory::class,
|
||||||
|
ShortUrl\Entity\ShortUrl::class,
|
||||||
|
],
|
||||||
|
|
||||||
Tag\TagService::class => ConfigAbstractFactory::class,
|
Tag\TagService::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\ShortUrl\Model;
|
||||||
|
|
||||||
|
final readonly class ExpiredShortUrlsConditions
|
||||||
|
{
|
||||||
|
public function __construct(public bool $pastValidUntil = true, public bool $maxVisitsReached = false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromQuery(array $query): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
pastValidUntil: (bool) ($query['pastValidUntil'] ?? true),
|
||||||
|
maxVisitsReached: (bool) ($query['maxVisitsReached'] ?? true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasConditions(): bool
|
||||||
|
{
|
||||||
|
return $this->pastValidUntil || $this->maxVisitsReached;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\ShortUrl\Repository;
|
||||||
|
|
||||||
|
use Cake\Chronos\Chronos;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ExpiredShortUrlsConditions;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Entity\ShortUrlVisitsCount;
|
||||||
|
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
class ExpiredShortUrlsRepository extends EntitySpecificationRepository implements ExpiredShortUrlsRepositoryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function delete(ExpiredShortUrlsConditions $conditions = new ExpiredShortUrlsConditions()): int
|
||||||
|
{
|
||||||
|
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||||
|
$qb->delete(ShortUrl::class, 's');
|
||||||
|
|
||||||
|
return $this->applyConditions($qb, $conditions, fn () => (int) $qb->getQuery()->execute());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function dryCount(ExpiredShortUrlsConditions $conditions = new ExpiredShortUrlsConditions()): int
|
||||||
|
{
|
||||||
|
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||||
|
$qb->select('COUNT(s.id)')
|
||||||
|
->from(ShortUrl::class, 's');
|
||||||
|
|
||||||
|
return $this->applyConditions($qb, $conditions, fn () => (int) $qb->getQuery()->getSingleScalarResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callable(): int $getResultFromQueryBuilder
|
||||||
|
*/
|
||||||
|
private function applyConditions(
|
||||||
|
QueryBuilder $qb,
|
||||||
|
ExpiredShortUrlsConditions $conditions,
|
||||||
|
callable $getResultFromQueryBuilder,
|
||||||
|
): int {
|
||||||
|
if (! $conditions->hasConditions()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($conditions->pastValidUntil) {
|
||||||
|
$qb
|
||||||
|
->where($qb->expr()->andX(
|
||||||
|
$qb->expr()->isNotNull('s.validUntil'),
|
||||||
|
$qb->expr()->lt('s.validUntil', ':now'),
|
||||||
|
))
|
||||||
|
->setParameter('now', Chronos::now()->toDateTimeString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($conditions->maxVisitsReached) {
|
||||||
|
$qb->orWhere($qb->expr()->andX(
|
||||||
|
$qb->expr()->isNotNull('s.maxVisits'),
|
||||||
|
$qb->expr()->lte(
|
||||||
|
's.maxVisits',
|
||||||
|
sprintf(
|
||||||
|
'(SELECT COALESCE(SUM(vc.count), 0) FROM %s as vc WHERE vc.shortUrl=s)',
|
||||||
|
ShortUrlVisitsCount::class,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $getResultFromQueryBuilder();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\ShortUrl\Repository;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ExpiredShortUrlsConditions;
|
||||||
|
|
||||||
|
interface ExpiredShortUrlsRepositoryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Delete expired short URLs matching provided conditions
|
||||||
|
*/
|
||||||
|
public function delete(ExpiredShortUrlsConditions $conditions = new ExpiredShortUrlsConditions()): int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count how many expired short URLs would be deleted for provided conditions
|
||||||
|
*/
|
||||||
|
public function dryCount(ExpiredShortUrlsConditions $conditions = new ExpiredShortUrlsConditions()): int;
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioDbTest\Shlink\Core\ShortUrl\Repository;
|
||||||
|
|
||||||
|
use Cake\Chronos\Chronos;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use PHPUnit\Framework\Attributes\TestWith;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ExpiredShortUrlsConditions;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Repository\ExpiredShortUrlsRepository;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||||
|
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
||||||
|
|
||||||
|
class DeleteExpiredShortUrlsRepositoryTest extends DatabaseTestCase
|
||||||
|
{
|
||||||
|
private ExpiredShortUrlsRepository $repository;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$this->repository = new ExpiredShortUrlsRepository($em, $em->getClassMetadata(ShortUrl::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[TestWith([new ExpiredShortUrlsConditions(pastValidUntil: false, maxVisitsReached: false), 0])]
|
||||||
|
#[TestWith([new ExpiredShortUrlsConditions(pastValidUntil: true, maxVisitsReached: false), 7])]
|
||||||
|
#[TestWith([new ExpiredShortUrlsConditions(pastValidUntil: false, maxVisitsReached: true), 6])]
|
||||||
|
#[TestWith([new ExpiredShortUrlsConditions(pastValidUntil: true, maxVisitsReached: true), 9])]
|
||||||
|
public function deletesExpectedAmountOfShortUrls(
|
||||||
|
ExpiredShortUrlsConditions $conditions,
|
||||||
|
int $expectedDeletedShortUrls,
|
||||||
|
): void {
|
||||||
|
$createdShortUrls = $this->createDataSet();
|
||||||
|
|
||||||
|
self::assertEquals($expectedDeletedShortUrls, $this->repository->delete($conditions));
|
||||||
|
self::assertEquals(
|
||||||
|
$createdShortUrls - $expectedDeletedShortUrls,
|
||||||
|
$this->getEntityManager()->getRepository(ShortUrl::class)->count(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[TestWith([new ExpiredShortUrlsConditions(pastValidUntil: false, maxVisitsReached: false), 0])]
|
||||||
|
#[TestWith([new ExpiredShortUrlsConditions(pastValidUntil: true, maxVisitsReached: false), 7])]
|
||||||
|
#[TestWith([new ExpiredShortUrlsConditions(pastValidUntil: false, maxVisitsReached: true), 6])]
|
||||||
|
#[TestWith([new ExpiredShortUrlsConditions(pastValidUntil: true, maxVisitsReached: true), 9])]
|
||||||
|
public function countsExpectedAmountOfShortUrls(
|
||||||
|
ExpiredShortUrlsConditions $conditions,
|
||||||
|
int $expectedShortUrlsCount,
|
||||||
|
): void {
|
||||||
|
$createdShortUrls = $this->createDataSet();
|
||||||
|
|
||||||
|
self::assertEquals($expectedShortUrlsCount, $this->repository->dryCount($conditions));
|
||||||
|
self::assertEquals($createdShortUrls, $this->getEntityManager()->getRepository(ShortUrl::class)->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createDataSet(): int
|
||||||
|
{
|
||||||
|
// Create some non-expired short URLs
|
||||||
|
$this->createShortUrls(5);
|
||||||
|
$this->createShortUrls(2, [ShortUrlInputFilter::VALID_UNTIL => Chronos::now()->addDays(1)->toAtomString()]);
|
||||||
|
$this->createShortUrls(3, [ShortUrlInputFilter::MAX_VISITS => 4], visitsPerShortUrl: 2);
|
||||||
|
|
||||||
|
// Create some short URLs with a valid date in the past
|
||||||
|
$this->createShortUrls(3, [ShortUrlInputFilter::VALID_UNTIL => Chronos::now()->subDays(1)->toAtomString()]);
|
||||||
|
|
||||||
|
// Create some short URLs which reached the max amount of visits
|
||||||
|
$this->createShortUrls(2, [ShortUrlInputFilter::MAX_VISITS => 3], visitsPerShortUrl: 3);
|
||||||
|
|
||||||
|
// Create some short URLs with a valid date in the past which also reached the max amount of visits
|
||||||
|
$this->createShortUrls(4, [
|
||||||
|
ShortUrlInputFilter::VALID_UNTIL => Chronos::now()->subDays(1)->toAtomString(),
|
||||||
|
ShortUrlInputFilter::MAX_VISITS => 3,
|
||||||
|
], visitsPerShortUrl: 4);
|
||||||
|
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
|
return 5 + 2 + 3 + 3 + 2 + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createShortUrls(int $amountOfShortUrls, array $metadata = [], int $visitsPerShortUrl = 0): void
|
||||||
|
{
|
||||||
|
for ($i = 0; $i < $amountOfShortUrls; $i++) {
|
||||||
|
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
|
ShortUrlInputFilter::LONG_URL => 'https://shlink.io',
|
||||||
|
...$metadata,
|
||||||
|
]));
|
||||||
|
$this->getEntityManager()->persist($shortUrl);
|
||||||
|
|
||||||
|
for ($j = 0; $j < $visitsPerShortUrl; $j++) {
|
||||||
|
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue