Improved public API in ShortUrl entity, reducing anemic model

This commit is contained in:
Alejandro Celaya 2018-10-28 16:00:54 +01:00
parent 877b098b09
commit 8e1e8ba7de
26 changed files with 96 additions and 148 deletions

View File

@ -62,7 +62,7 @@ class GeneratePreviewCommand extends Command
$page += 1;
foreach ($shortUrls as $shortUrl) {
$this->processUrl($shortUrl->getOriginalUrl(), $output);
$this->processUrl($shortUrl->getLongUrl(), $output);
}
} while ($page <= $shortUrls->count());

View File

@ -56,9 +56,9 @@ class GeneratePreviewCommandTest extends TestCase
public function previewsForEveryUrlAreGenerated()
{
$paginator = $this->createPaginator([
(new ShortUrl())->setOriginalUrl('http://foo.com'),
(new ShortUrl())->setOriginalUrl('https://bar.com'),
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
new ShortUrl('http://foo.com'),
new ShortUrl('https://bar.com'),
new ShortUrl('http://baz.com/something'),
]);
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
@ -77,9 +77,9 @@ class GeneratePreviewCommandTest extends TestCase
public function exceptionWillOutputError()
{
$items = [
(new ShortUrl())->setOriginalUrl('http://foo.com'),
(new ShortUrl())->setOriginalUrl('https://bar.com'),
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
new ShortUrl('http://foo.com'),
new ShortUrl('https://bar.com'),
new ShortUrl('http://baz.com/something'),
];
$paginator = $this->createPaginator($items);
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);

View File

@ -45,8 +45,7 @@ class GenerateShortcodeCommandTest extends TestCase
{
$this->urlShortener->urlToShortCode(Argument::cetera())
->willReturn(
(new ShortUrl())->setShortCode('abc123')
->setLongUrl('')
(new ShortUrl(''))->setShortCode('abc123')
)
->shouldBeCalledTimes(1);

View File

@ -55,7 +55,7 @@ class ListShortcodesCommandTest extends TestCase
// The paginator will return more than one page for the first 3 times
$data = [];
for ($i = 0; $i < 50; $i++) {
$data[] = (new ShortUrl())->setLongUrl('url_' . $i);
$data[] = new ShortUrl('url_' . $i);
}
$this->shortUrlService->listShortUrls(Argument::cetera())->will(function () use (&$data) {
@ -74,7 +74,7 @@ class ListShortcodesCommandTest extends TestCase
// The paginator will return more than one page
$data = [];
for ($i = 0; $i < 30; $i++) {
$data[] = (new ShortUrl())->setLongUrl('url_' . $i);
$data[] = new ShortUrl('url_' . $i);
}
$this->shortUrlService->listShortUrls(Argument::cetera())->willReturn(new Paginator(new ArrayAdapter($data)))

View File

@ -43,7 +43,7 @@ class ResolveUrlCommandTest extends TestCase
{
$shortCode = 'abc123';
$expectedUrl = 'http://domain.com/foo/bar';
$shortUrl = (new ShortUrl())->setLongUrl($expectedUrl);
$shortUrl = new ShortUrl($expectedUrl);
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)
->shouldBeCalledTimes(1);

View File

@ -8,6 +8,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
use function count;
@ -25,7 +26,7 @@ class ShortUrl extends AbstractEntity
* @var string
* @ORM\Column(name="original_url", type="string", nullable=false, length=1024)
*/
private $originalUrl;
private $longUrl;
/**
* @var string
* @ORM\Column(
@ -73,39 +74,23 @@ class ShortUrl extends AbstractEntity
*/
private $maxVisits;
public function __construct()
public function __construct(string $longUrl, ShortUrlMeta $meta = null)
{
$this->shortCode = '';
$meta = $meta ?? ShortUrlMeta::createEmpty();
$this->longUrl = $longUrl;
$this->dateCreated = Chronos::now();
$this->visits = new ArrayCollection();
$this->tags = new ArrayCollection();
$this->validSince = $meta->getValidSince();
$this->validUntil = $meta->getValidUntil();
$this->maxVisits = $meta->getMaxVisits();
$this->shortCode = $meta->getCustomSlug() ?? ''; // TODO logic to calculate short code should be passed somehow
}
public function getLongUrl(): string
{
return $this->originalUrl;
}
public function setLongUrl(string $longUrl): self
{
$this->originalUrl = $longUrl;
return $this;
}
/**
* @deprecated Use getLongUrl() instead
*/
public function getOriginalUrl(): string
{
return $this->getLongUrl();
}
/**
* @deprecated Use setLongUrl() instead
*/
public function setOriginalUrl(string $originalUrl): self
{
return $this->setLongUrl($originalUrl);
return $this->longUrl;
}
public function getShortCode(): string
@ -113,6 +98,7 @@ class ShortUrl extends AbstractEntity
return $this->shortCode;
}
// TODO Short code is currently calculated based on the ID, so a setter is needed
public function setShortCode(string $shortCode): self
{
$this->shortCode = $shortCode;
@ -124,12 +110,6 @@ class ShortUrl extends AbstractEntity
return $this->dateCreated;
}
public function setDateCreated(Chronos $dateCreated): self
{
$this->dateCreated = $dateCreated;
return $this;
}
/**
* @return Collection|Tag[]
*/
@ -147,10 +127,17 @@ class ShortUrl extends AbstractEntity
return $this;
}
public function addTag(Tag $tag): self
public function updateMeta(ShortUrlMeta $shortCodeMeta): void
{
$this->tags->add($tag);
return $this;
if ($shortCodeMeta->hasValidSince()) {
$this->validSince = $shortCodeMeta->getValidSince();
}
if ($shortCodeMeta->hasValidUntil()) {
$this->validUntil = $shortCodeMeta->getValidUntil();
}
if ($shortCodeMeta->hasMaxVisits()) {
$this->maxVisits = $shortCodeMeta->getMaxVisits();
}
}
public function getValidSince(): ?Chronos
@ -158,23 +145,11 @@ class ShortUrl extends AbstractEntity
return $this->validSince;
}
public function setValidSince(?Chronos $validSince): self
{
$this->validSince = $validSince;
return $this;
}
public function getValidUntil(): ?Chronos
{
return $this->validUntil;
}
public function setValidUntil(?Chronos $validUntil): self
{
$this->validUntil = $validUntil;
return $this;
}
public function getVisitsCount(): int
{
return count($this->visits);
@ -196,12 +171,6 @@ class ShortUrl extends AbstractEntity
return $this->maxVisits;
}
public function setMaxVisits(?int $maxVisits): self
{
$this->maxVisits = $maxVisits;
return $this;
}
public function maxVisitsReached(): bool
{
return $this->maxVisits !== null && $this->getVisitsCount() >= $this->maxVisits;

View File

@ -32,9 +32,13 @@ final class ShortUrlMeta
{
}
public static function createEmpty(): self
{
return new self();
}
/**
* @param array $data
* @return ShortUrlMeta
* @throws ValidationException
*/
public static function createFromRawData(array $data): self
@ -49,7 +53,6 @@ final class ShortUrlMeta
* @param string|Chronos|null $validUntil
* @param string|null $customSlug
* @param int|null $maxVisits
* @return ShortUrlMeta
* @throws ValidationException
*/
public static function createFromParams(
@ -124,10 +127,7 @@ final class ShortUrlMeta
return $this->validUntil !== null;
}
/**
* @return null|string
*/
public function getCustomSlug()
public function getCustomSlug(): ?string
{
return $this->customSlug;
}
@ -137,10 +137,7 @@ final class ShortUrlMeta
return $this->customSlug !== null;
}
/**
* @return int|null
*/
public function getMaxVisits()
public function getMaxVisits(): ?int
{
return $this->maxVisits;
}

View File

@ -55,8 +55,8 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
{
// Map public field names to column names
$fieldNameMap = [
'originalUrl' => 'originalUrl',
'longUrl' => 'originalUrl',
'originalUrl' => 'longUrl',
'longUrl' => 'longUrl',
'shortCode' => 'shortCode',
'dateCreated' => 'dateCreated',
];
@ -112,7 +112,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
}
$conditions = [
$qb->expr()->like('s.originalUrl', ':searchPattern'),
$qb->expr()->like('s.longUrl', ':searchPattern'),
$qb->expr()->like('s.shortCode', ':searchPattern'),
$qb->expr()->like('t.name', ':searchPattern'),
];

View File

@ -60,18 +60,10 @@ class ShortUrlService implements ShortUrlServiceInterface
/**
* @throws InvalidShortCodeException
*/
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortCodeMeta): ShortUrl
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortUrlMeta): ShortUrl
{
$shortUrl = $this->findByShortCode($this->em, $shortCode);
if ($shortCodeMeta->hasValidSince()) {
$shortUrl->setValidSince($shortCodeMeta->getValidSince());
}
if ($shortCodeMeta->hasValidUntil()) {
$shortUrl->setValidUntil($shortCodeMeta->getValidUntil());
}
if ($shortCodeMeta->hasMaxVisits()) {
$shortUrl->setMaxVisits($shortCodeMeta->getMaxVisits());
}
$shortUrl->updateMeta($shortUrlMeta);
/** @var ORM\EntityManager $em */
$em = $this->em;

View File

@ -26,5 +26,5 @@ interface ShortUrlServiceInterface
/**
* @throws InvalidShortCodeException
*/
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortCodeMeta): ShortUrl;
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortUrlMeta): ShortUrl;
}

View File

@ -16,6 +16,7 @@ use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Exception\RuntimeException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
use Throwable;
@ -90,15 +91,15 @@ class UrlShortener implements UrlShortenerInterface
$this->em->beginTransaction();
// First, create the short URL with an empty short code
$shortUrl = new ShortUrl();
$shortUrl->setOriginalUrl((string) $url)
->setValidSince($validSince)
->setValidUntil($validUntil)
->setMaxVisits($maxVisits);
$shortUrl = new ShortUrl(
(string) $url,
ShortUrlMeta::createFromParams($validSince, $validUntil, null, $maxVisits)
);
$this->em->persist($shortUrl);
$this->em->flush();
// Generate the short code and persist it
// TODO Somehow provide the logic to calculate the shortCode to avoid the need of a setter
$shortCode = $customSlug ?? $this->convertAutoincrementIdToShortCode((float) $shortUrl->getId());
$shortUrl->setShortCode($shortCode)
->setTags($this->tagNamesToEntities($this->em, $tags));

View File

@ -8,6 +8,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Tag;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
use ShlinkioTest\Shlink\Common\DbUnit\DatabaseTestCase;
use function count;
@ -35,15 +36,12 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
*/
public function findOneByShortCodeReturnsProperData()
{
$foo = new ShortUrl();
$foo->setOriginalUrl('foo')
->setShortCode('foo');
$foo = new ShortUrl('foo');
$foo->setShortCode('foo');
$this->getEntityManager()->persist($foo);
$bar = new ShortUrl();
$bar->setOriginalUrl('bar')
->setShortCode('bar_very_long_text')
->setValidSince(Chronos::now()->addMonth());
$bar = new ShortUrl('bar', ShortUrlMeta::createFromParams(Chronos::now()->addMonth()));
$bar->setShortCode('bar_very_long_text');
$this->getEntityManager()->persist($bar);
$visits = [];
@ -52,11 +50,9 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->persist($visit);
$visits[] = $visit;
}
$baz = new ShortUrl();
$baz->setOriginalUrl('baz')
->setShortCode('baz')
->setVisits(new ArrayCollection($visits))
->setMaxVisits(3);
$baz = new ShortUrl('baz', ShortUrlMeta::createFromRawData(['maxVisits' => 3]));
$baz->setShortCode('baz')
->setVisits(new ArrayCollection($visits));
$this->getEntityManager()->persist($baz);
$this->getEntityManager()->flush();
@ -75,8 +71,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
$count = 5;
for ($i = 0; $i < $count; $i++) {
$this->getEntityManager()->persist(
(new ShortUrl())->setOriginalUrl((string) $i)
->setShortCode((string) $i)
(new ShortUrl((string) $i))->setShortCode((string) $i)
);
}
$this->getEntityManager()->flush();
@ -92,20 +87,17 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
$tag = new Tag('bar');
$this->getEntityManager()->persist($tag);
$foo = new ShortUrl();
$foo->setOriginalUrl('foo')
->setShortCode('foo')
$foo = new ShortUrl('foo');
$foo->setShortCode('foo')
->setTags(new ArrayCollection([$tag]));
$this->getEntityManager()->persist($foo);
$bar = new ShortUrl();
$bar->setOriginalUrl('bar')
->setShortCode('bar_very_long_text');
$bar = new ShortUrl('bar');
$bar->setShortCode('bar_very_long_text');
$this->getEntityManager()->persist($bar);
$foo2 = new ShortUrl();
$foo2->setOriginalUrl('foo_2')
->setShortCode('foo_2');
$foo2 = new ShortUrl('foo_2');
$foo2->setShortCode('foo_2');
$this->getEntityManager()->persist($foo2);
$this->getEntityManager()->flush();
@ -123,8 +115,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
$urls = ['a', 'z', 'c', 'b'];
foreach ($urls as $url) {
$this->getEntityManager()->persist(
(new ShortUrl())->setShortCode($url)
->setLongUrl($url)
(new ShortUrl($url))->setShortCode($url)
);
}

View File

@ -56,8 +56,7 @@ class VisitRepositoryTest extends DatabaseTestCase
*/
public function findVisitsByShortUrlReturnsProperData()
{
$shortUrl = new ShortUrl();
$shortUrl->setOriginalUrl('');
$shortUrl = new ShortUrl('');
$this->getEntityManager()->persist($shortUrl);
for ($i = 0; $i < 6; $i++) {

View File

@ -50,7 +50,7 @@ class PixelActionTest extends TestCase
{
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(
(new ShortUrl())->setLongUrl('http://domain.com/foo/bar')
new ShortUrl('http://domain.com/foo/bar')
)->shouldBeCalledTimes(1);
$this->visitTracker->track(Argument::cetera())->shouldBeCalledTimes(1);

View File

@ -68,7 +68,7 @@ class PreviewActionTest extends TestCase
{
$shortCode = 'abc123';
$url = 'foobar.com';
$shortUrl = (new ShortUrl())->setLongUrl($url);
$shortUrl = new ShortUrl($url);
$path = __FILE__;
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)->shouldBeCalledTimes(1);
$this->previewGenerator->generatePreview($url)->willReturn($path)->shouldBeCalledTimes(1);

View File

@ -84,7 +84,7 @@ class QrCodeActionTest extends TestCase
public function aCorrectRequestReturnsTheQrCodeResponse()
{
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn((new ShortUrl())->setLongUrl(''))
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(new ShortUrl(''))
->shouldBeCalledTimes(1);
$delegate = $this->prophesize(RequestHandlerInterface::class);

View File

@ -52,7 +52,7 @@ class RedirectActionTest extends TestCase
{
$shortCode = 'abc123';
$expectedUrl = 'http://domain.com/foo/bar';
$shortUrl = (new ShortUrl())->setLongUrl($expectedUrl);
$shortUrl = new ShortUrl($expectedUrl);
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)
->shouldBeCalledTimes(1);
$this->visitTracker->track(Argument::cetera())->shouldBeCalledTimes(1);
@ -93,7 +93,7 @@ class RedirectActionTest extends TestCase
{
$shortCode = 'abc123';
$expectedUrl = 'http://domain.com/foo/bar';
$shortUrl = (new ShortUrl())->setLongUrl($expectedUrl);
$shortUrl = new ShortUrl($expectedUrl);
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)
->shouldBeCalledTimes(1);
$this->visitTracker->track(Argument::cetera())->shouldNotBeCalled();

View File

@ -30,10 +30,10 @@ class DeleteShortUrlServiceTest extends TestCase
public function setUp()
{
$shortUrl = (new ShortUrl())->setShortCode('abc123')
->setVisits(new ArrayCollection(array_map(function () {
return new Visit();
}, range(0, 10))));
$shortUrl = (new ShortUrl(''))->setShortCode('abc123')
->setVisits(new ArrayCollection(array_map(function () {
return new Visit();
}, range(0, 10))));
$this->em = $this->prophesize(EntityManagerInterface::class);

View File

@ -42,10 +42,10 @@ class ShortUrlServiceTest extends TestCase
public function listedUrlsAreReturnedFromEntityManager()
{
$list = [
new ShortUrl(),
new ShortUrl(),
new ShortUrl(),
new ShortUrl(),
new ShortUrl(''),
new ShortUrl(''),
new ShortUrl(''),
new ShortUrl(''),
];
$repo = $this->prophesize(ShortUrlRepository::class);
@ -98,7 +98,7 @@ class ShortUrlServiceTest extends TestCase
*/
public function updateMetadataByShortCodeUpdatesProvidedData()
{
$shortUrl = new ShortUrl();
$shortUrl = new ShortUrl('');
$repo = $this->prophesize(ShortUrlRepository::class);
$findShortUrl = $repo->findOneBy(['shortCode' => 'abc123'])->willReturn($shortUrl);

View File

@ -148,7 +148,7 @@ class UrlShortenerTest extends TestCase
$repo = $this->prophesize(ShortUrlRepositoryInterface::class);
/** @var MethodProphecy $findBySlug */
$findBySlug = $repo->findOneBy(['shortCode' => 'custom-slug'])->willReturn(new ShortUrl());
$findBySlug = $repo->findOneBy(['shortCode' => 'custom-slug'])->willReturn(new ShortUrl(''));
$repo->findOneBy(Argument::cetera())->willReturn(null);
/** @var MethodProphecy $getRepo */
$getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
@ -174,9 +174,8 @@ class UrlShortenerTest extends TestCase
{
// 12C1c -> 10
$shortCode = '12C1c';
$shortUrl = new ShortUrl();
$shortUrl->setShortCode($shortCode)
->setOriginalUrl('expected_url');
$shortUrl = new ShortUrl('expected_url');
$shortUrl->setShortCode($shortCode);
$repo = $this->prophesize(ShortUrlRepositoryInterface::class);
$repo->findOneByShortCode($shortCode)->willReturn($shortUrl);

View File

@ -38,7 +38,7 @@ class VisitsTrackerTest extends TestCase
{
$shortCode = '123ABC';
$repo = $this->prophesize(EntityRepository::class);
$repo->findOneBy(['shortCode' => $shortCode])->willReturn(new ShortUrl());
$repo->findOneBy(['shortCode' => $shortCode])->willReturn(new ShortUrl(''));
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1);
$this->em->persist(Argument::any())->shouldBeCalledTimes(1);
@ -55,7 +55,7 @@ class VisitsTrackerTest extends TestCase
$shortCode = '123ABC';
$test = $this;
$repo = $this->prophesize(EntityRepository::class);
$repo->findOneBy(['shortCode' => $shortCode])->willReturn(new ShortUrl());
$repo->findOneBy(['shortCode' => $shortCode])->willReturn(new ShortUrl(''));
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1);
$this->em->persist(Argument::any())->will(function ($args) use ($test) {
@ -74,7 +74,7 @@ class VisitsTrackerTest extends TestCase
public function infoReturnsVisistForCertainShortCode()
{
$shortCode = '123ABC';
$shortUrl = (new ShortUrl())->setOriginalUrl('http://domain.com/foo/bar');
$shortUrl = new ShortUrl('http://domain.com/foo/bar');
$repo = $this->prophesize(EntityRepository::class);
$repo->findOneBy(['shortCode' => $shortCode])->willReturn($shortUrl);
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1);

View File

@ -54,8 +54,7 @@ class CreateShortUrlActionTest extends TestCase
{
$this->urlShortener->urlToShortCode(Argument::type(Uri::class), Argument::type('array'), Argument::cetera())
->willReturn(
(new ShortUrl())->setShortCode('abc123')
->setLongUrl('')
(new ShortUrl(''))->setShortCode('abc123')
)
->shouldBeCalledTimes(1);

View File

@ -82,7 +82,9 @@ class EditShortUrlActionTest extends TestCase
->withParsedBody([
'maxVisits' => 5,
]);
$updateMeta = $this->shortUrlService->updateMetadataByShortCode(Argument::cetera())->willReturn(new ShortUrl());
$updateMeta = $this->shortUrlService->updateMetadataByShortCode(Argument::cetera())->willReturn(
new ShortUrl('')
);
$resp = $this->action->handle($request);

View File

@ -60,7 +60,7 @@ class EditShortUrlTagsActionTest extends TestCase
public function tagsListIsReturnedIfCorrectShortCodeIsProvided()
{
$shortCode = 'abc123';
$this->shortUrlService->setTagsByShortCode($shortCode, [])->willReturn(new ShortUrl())
$this->shortUrlService->setTagsByShortCode($shortCode, [])->willReturn(new ShortUrl(''))
->shouldBeCalledTimes(1);
$response = $this->action->handle(

View File

@ -55,7 +55,7 @@ class ResolveShortUrlActionTest extends TestCase
{
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(
(new ShortUrl())->setLongUrl('http://domain.com/foo/bar')
new ShortUrl('http://domain.com/foo/bar')
)->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);

View File

@ -103,7 +103,7 @@ class SingleStepCreateShortUrlActionTest extends TestCase
null,
null,
null
)->willReturn((new ShortUrl())->setLongUrl(''));
)->willReturn(new ShortUrl(''));
$resp = $this->action->handle($request);