mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-25 18:45:27 -06:00
Created specific service to delete short URLs
This commit is contained in:
parent
394d9ff4d2
commit
159529937d
@ -175,7 +175,7 @@ class ShortUrl extends AbstractEntity
|
|||||||
|
|
||||||
public function getVisitsCount(): int
|
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)
|
public static function fromCharset($shortCode, $charSet, \Exception $previous = null)
|
||||||
{
|
{
|
||||||
$code = isset($previous) ? $previous->getCode() : -1;
|
$code = $previous !== null ? $previous->getCode() : -1;
|
||||||
return new static(
|
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,
|
$code,
|
||||||
$previous
|
$previous
|
||||||
);
|
);
|
||||||
@ -17,6 +17,6 @@ class InvalidShortCodeException extends RuntimeException
|
|||||||
|
|
||||||
public static function fromNotFoundShortCode($shortCode)
|
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\Exception\InvalidShortCodeException;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||||
|
use Shlinkio\Shlink\Core\Service\ShortUrl\FindShortCodeTrait;
|
||||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||||
use Zend\Paginator\Paginator;
|
use Zend\Paginator\Paginator;
|
||||||
|
|
||||||
class ShortUrlService implements ShortUrlServiceInterface
|
class ShortUrlService implements ShortUrlServiceInterface
|
||||||
{
|
{
|
||||||
|
use FindShortCodeTrait;
|
||||||
use TagManagerTrait;
|
use TagManagerTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,7 +50,7 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||||||
*/
|
*/
|
||||||
public function setTagsByShortCode(string $shortCode, array $tags = []): ShortUrl
|
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));
|
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
@ -60,7 +62,7 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||||||
*/
|
*/
|
||||||
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortCodeMeta): ShortUrl
|
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortCodeMeta): ShortUrl
|
||||||
{
|
{
|
||||||
$shortUrl = $this->findByShortCode($shortCode);
|
$shortUrl = $this->findByShortCode($this->em, $shortCode);
|
||||||
if ($shortCodeMeta->hasValidSince()) {
|
if ($shortCodeMeta->hasValidSince()) {
|
||||||
$shortUrl->setValidSince($shortCodeMeta->getValidSince());
|
$shortUrl->setValidSince($shortCodeMeta->getValidSince());
|
||||||
}
|
}
|
||||||
@ -77,31 +79,4 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||||||
|
|
||||||
return $shortUrl;
|
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
|
* @throws InvalidShortCodeException
|
||||||
*/
|
*/
|
||||||
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortCodeMeta): ShortUrl;
|
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…
Reference in New Issue
Block a user