Created specific service to delete short URLs

This commit is contained in:
Alejandro Celaya 2018-09-15 11:54:58 +02:00
parent 394d9ff4d2
commit 159529937d
10 changed files with 300 additions and 38 deletions

View file

@ -175,7 +175,7 @@ class ShortUrl extends AbstractEntity
public function getVisitsCount(): int
{
return count($this->visits);
return \count($this->visits);
}
/**

View 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;
}
}

View file

@ -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));
}
}

View 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();
}
}

View file

@ -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;
}

View 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;
}
}

View file

@ -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;
}
}

View file

@ -27,9 +27,4 @@ interface ShortUrlServiceInterface
* @throws InvalidShortCodeException
*/
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortCodeMeta): ShortUrl;
/**
* @throws InvalidShortCodeException
*/
public function deleteByShortCode(string $shortCode): void;
}

View 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));
}
}

View file

@ -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,
]));
}
}