mirror of
https://github.com/shlinkio/shlink.git
synced 2025-03-14 04:00:57 +03:00
Created specific service to delete short URLs
This commit is contained in:
parent
394d9ff4d2
commit
159529937d
10 changed files with 300 additions and 38 deletions
|
@ -175,7 +175,7 @@ class ShortUrl extends AbstractEntity
|
|||
|
||||
public function getVisitsCount(): int
|
||||
{
|
||||
return count($this->visits);
|
||||
return \count($this->visits);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
34
module/Core/src/Exception/DeleteShortUrlException.php
Normal file
34
module/Core/src/Exception/DeleteShortUrlException.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class DeleteShortUrlException extends RuntimeException
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $visitsThreshold;
|
||||
|
||||
public function __construct(int $visitsThreshold, string $message = '', int $code = 0, Throwable $previous = null)
|
||||
{
|
||||
$this->visitsThreshold = $visitsThreshold;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public static function fromVisitsThreshold(int $threshold, string $shortCode): self
|
||||
{
|
||||
return new self($threshold, \sprintf(
|
||||
'Impossible to delete short URL with short code "%s" since it has more than "%s" visits.',
|
||||
$shortCode,
|
||||
$threshold
|
||||
));
|
||||
}
|
||||
|
||||
public function getVisitsThreshold(): int
|
||||
{
|
||||
return $this->visitsThreshold;
|
||||
}
|
||||
}
|
|
@ -7,9 +7,9 @@ class InvalidShortCodeException extends RuntimeException
|
|||
{
|
||||
public static function fromCharset($shortCode, $charSet, \Exception $previous = null)
|
||||
{
|
||||
$code = isset($previous) ? $previous->getCode() : -1;
|
||||
$code = $previous !== null ? $previous->getCode() : -1;
|
||||
return new static(
|
||||
sprintf('Provided short code "%s" does not match the char set "%s"', $shortCode, $charSet),
|
||||
\sprintf('Provided short code "%s" does not match the char set "%s"', $shortCode, $charSet),
|
||||
$code,
|
||||
$previous
|
||||
);
|
||||
|
@ -17,6 +17,6 @@ class InvalidShortCodeException extends RuntimeException
|
|||
|
||||
public static function fromNotFoundShortCode($shortCode)
|
||||
{
|
||||
return new static(sprintf('Provided short code "%s" does not belong to a short URL', $shortCode));
|
||||
return new static(\sprintf('Provided short code "%s" does not belong to a short URL', $shortCode));
|
||||
}
|
||||
}
|
||||
|
|
56
module/Core/src/Service/ShortUrl/DeleteShortUrlService.php
Normal file
56
module/Core/src/Service/ShortUrl/DeleteShortUrlService.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Service\ShortUrl;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Options\DeleteShortUrlsOptions;
|
||||
|
||||
class DeleteShortUrlService implements DeleteShortUrlServiceInterface
|
||||
{
|
||||
use FindShortCodeTrait;
|
||||
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
private $em;
|
||||
/**
|
||||
* @var DeleteShortUrlsOptions
|
||||
*/
|
||||
private $deleteShortUrlsOptions;
|
||||
|
||||
public function __construct(EntityManagerInterface $em, DeleteShortUrlsOptions $deleteShortUrlsOptions)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->deleteShortUrlsOptions = $deleteShortUrlsOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception\InvalidShortCodeException
|
||||
* @throws Exception\DeleteShortUrlException
|
||||
*/
|
||||
public function deleteByShortCode(string $shortCode): void
|
||||
{
|
||||
$shortUrl = $this->findByShortCode($this->em, $shortCode);
|
||||
if ($this->isThresholdReached($shortUrl)) {
|
||||
throw Exception\DeleteShortUrlException::fromVisitsThreshold(
|
||||
$this->deleteShortUrlsOptions->getVisitsThreshold(),
|
||||
$shortUrl->getShortCode()
|
||||
);
|
||||
}
|
||||
|
||||
$this->em->remove($shortUrl);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
private function isThresholdReached(ShortUrl $shortUrl): bool
|
||||
{
|
||||
if (! $this->deleteShortUrlsOptions->doCheckVisitsThreshold()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $shortUrl->getVisitsCount() >= $this->deleteShortUrlsOptions->getVisitsThreshold();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Service\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
interface DeleteShortUrlServiceInterface
|
||||
{
|
||||
/**
|
||||
* @throws Exception\InvalidShortCodeException
|
||||
* @throws Exception\DeleteShortUrlException
|
||||
*/
|
||||
public function deleteByShortCode(string $shortCode): void;
|
||||
}
|
29
module/Core/src/Service/ShortUrl/FindShortCodeTrait.php
Normal file
29
module/Core/src/Service/ShortUrl/FindShortCodeTrait.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Service\ShortUrl;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
|
||||
trait FindShortCodeTrait
|
||||
{
|
||||
/**
|
||||
* @param string $shortCode
|
||||
* @return ShortUrl
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
private function findByShortCode(EntityManagerInterface $em, string $shortCode): ShortUrl
|
||||
{
|
||||
/** @var ShortUrl|null $shortUrl */
|
||||
$shortUrl = $em->getRepository(ShortUrl::class)->findOneBy([
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
if ($shortUrl === null) {
|
||||
throw InvalidShortCodeException::fromNotFoundShortCode($shortCode);
|
||||
}
|
||||
|
||||
return $shortUrl;
|
||||
}
|
||||
}
|
|
@ -9,11 +9,13 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
|||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\FindShortCodeTrait;
|
||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
class ShortUrlService implements ShortUrlServiceInterface
|
||||
{
|
||||
use FindShortCodeTrait;
|
||||
use TagManagerTrait;
|
||||
|
||||
/**
|
||||
|
@ -48,7 +50,7 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||
*/
|
||||
public function setTagsByShortCode(string $shortCode, array $tags = []): ShortUrl
|
||||
{
|
||||
$shortUrl = $this->findByShortCode($shortCode);
|
||||
$shortUrl = $this->findByShortCode($this->em, $shortCode);
|
||||
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
|
||||
$this->em->flush();
|
||||
|
||||
|
@ -60,7 +62,7 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||
*/
|
||||
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortCodeMeta): ShortUrl
|
||||
{
|
||||
$shortUrl = $this->findByShortCode($shortCode);
|
||||
$shortUrl = $this->findByShortCode($this->em, $shortCode);
|
||||
if ($shortCodeMeta->hasValidSince()) {
|
||||
$shortUrl->setValidSince($shortCodeMeta->getValidSince());
|
||||
}
|
||||
|
@ -77,31 +79,4 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||
|
||||
return $shortUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
public function deleteByShortCode(string $shortCode): void
|
||||
{
|
||||
$this->em->remove($this->findByShortCode($shortCode));
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $shortCode
|
||||
* @return ShortUrl
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
private function findByShortCode(string $shortCode): ShortUrl
|
||||
{
|
||||
/** @var ShortUrl|null $shortUrl */
|
||||
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
if ($shortUrl === null) {
|
||||
throw InvalidShortCodeException::fromNotFoundShortCode($shortCode);
|
||||
}
|
||||
|
||||
return $shortUrl;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,9 +27,4 @@ interface ShortUrlServiceInterface
|
|||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortCodeMeta): ShortUrl;
|
||||
|
||||
/**
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
public function deleteByShortCode(string $shortCode): void;
|
||||
}
|
||||
|
|
61
module/Core/test/Exception/DeleteShortUrlExceptionTest.php
Normal file
61
module/Core/test/Exception/DeleteShortUrlExceptionTest.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Exception;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Exception\DeleteShortUrlException;
|
||||
|
||||
class DeleteShortUrlExceptionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideMessages
|
||||
*/
|
||||
public function fromVisitsThresholdGeneratesMessageProperly(
|
||||
int $threshold,
|
||||
string $shortCode,
|
||||
string $expectedMessage
|
||||
) {
|
||||
$e = DeleteShortUrlException::fromVisitsThreshold($threshold, $shortCode);
|
||||
$this->assertEquals($expectedMessage, $e->getMessage());
|
||||
}
|
||||
|
||||
public function provideMessages(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
50,
|
||||
'abc123',
|
||||
'Impossible to delete short URL with short code "abc123" since it has more than "50" visits.',
|
||||
],
|
||||
[
|
||||
33,
|
||||
'def456',
|
||||
'Impossible to delete short URL with short code "def456" since it has more than "33" visits.',
|
||||
],
|
||||
[
|
||||
5713,
|
||||
'foobar',
|
||||
'Impossible to delete short URL with short code "foobar" since it has more than "5713" visits.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideThresholds
|
||||
*/
|
||||
public function visitsThresholdIsProperlyReturned(int $threshold)
|
||||
{
|
||||
$e = new DeleteShortUrlException($threshold);
|
||||
$this->assertEquals($threshold, $e->getVisitsThreshold());
|
||||
}
|
||||
|
||||
public function provideThresholds(): array
|
||||
{
|
||||
return \array_map(function (int $number) {
|
||||
return [$number];
|
||||
}, \range(5, 50, 5));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Service\ShortUrl;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Exception\DeleteShortUrlException;
|
||||
use Shlinkio\Shlink\Core\Options\DeleteShortUrlsOptions;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlService;
|
||||
|
||||
class DeleteShortUrlServiceTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var DeleteShortUrlService
|
||||
*/
|
||||
private $service;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $em;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$shortUrl = (new ShortUrl())->setShortCode('abc123')
|
||||
->setVisits(new ArrayCollection(\array_map(function () {
|
||||
return new Visit();
|
||||
}, \range(0, 10))));
|
||||
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
|
||||
$repo = $this->prophesize(ShortUrlRepositoryInterface::class);
|
||||
$repo->findOneBy(Argument::type('array'))->willReturn($shortUrl);
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function deleteByShortCodeThrowsExceptionWhenThresholdIsReached()
|
||||
{
|
||||
$service = $this->createService();
|
||||
|
||||
$this->expectException(DeleteShortUrlException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Impossible to delete short URL with short code "abc123" since it has more than "5" visits.'
|
||||
);
|
||||
|
||||
$service->deleteByShortCode('abc123');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function deleteByShortCodeDeletesUrlWhenThresholdIsReachedButCheckIsDisabled()
|
||||
{
|
||||
$service = $this->createService(false);
|
||||
|
||||
$remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null);
|
||||
$flush = $this->em->flush()->willReturn(null);
|
||||
|
||||
$service->deleteByShortCode('abc123');
|
||||
|
||||
$remove->shouldHaveBeenCalledTimes(1);
|
||||
$flush->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function deleteByShortCodeDeletesUrlWhenThresholdIsNotReached()
|
||||
{
|
||||
$service = $this->createService(true, 100);
|
||||
|
||||
$remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null);
|
||||
$flush = $this->em->flush()->willReturn(null);
|
||||
|
||||
$service->deleteByShortCode('abc123');
|
||||
|
||||
$remove->shouldHaveBeenCalledTimes(1);
|
||||
$flush->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
private function createService(bool $checkVisitsThreshold = true, int $visitsThreshold = 5): DeleteShortUrlService
|
||||
{
|
||||
return new DeleteShortUrlService($this->em->reveal(), new DeleteShortUrlsOptions([
|
||||
'visitsThreshold' => $visitsThreshold,
|
||||
'checkVisitsThreshold' => $checkVisitsThreshold,
|
||||
]));
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue