mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-25 18:45:27 -06:00
Implemented UrlShortener main service
This commit is contained in:
parent
93c2c1298a
commit
a9813b1ab9
28
.travis.yml
28
.travis.yml
@ -1,28 +0,0 @@
|
|||||||
sudo: false
|
|
||||||
|
|
||||||
language: php
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
fast_finish: true
|
|
||||||
include:
|
|
||||||
- php: 5.5
|
|
||||||
- php: 5.6
|
|
||||||
env:
|
|
||||||
- EXECUTE_CS_CHECK=true
|
|
||||||
- php: 7
|
|
||||||
- php: hhvm
|
|
||||||
allow_failures:
|
|
||||||
- php: hhvm
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- composer self-update
|
|
||||||
|
|
||||||
install:
|
|
||||||
- travis_retry composer install --no-interaction --ignore-platform-reqs --prefer-source --no-scripts
|
|
||||||
|
|
||||||
script:
|
|
||||||
- composer test
|
|
||||||
- if [[ $EXECUTE_CS_CHECK == 'true' ]]; then composer cs ; fi
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
email: true
|
|
@ -47,6 +47,6 @@
|
|||||||
"cs-fix": "phpcbf",
|
"cs-fix": "phpcbf",
|
||||||
"serve": "php -S 0.0.0.0:8000 -t public/",
|
"serve": "php -S 0.0.0.0:8000 -t public/",
|
||||||
"test": "phpunit",
|
"test": "phpunit",
|
||||||
"pretty-test": "phpunit -c tests/phpunit.xml --coverage-html build/coverage"
|
"pretty-test": "phpunit --coverage-html build/coverage"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<phpunit bootstrap="./vendor/autoload.php" colors="true">
|
<phpunit bootstrap="./vendor/autoload.php" colors="true">
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="App\\Tests">
|
<testsuite name="AcelayaTest">
|
||||||
<directory>./test</directory>
|
<directory>./tests</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
||||||
|
@ -41,9 +41,9 @@ class ShortUrl extends AbstractEntity
|
|||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->dateCreated = new \DateTime();
|
$this->setDateCreated(new \DateTime());
|
||||||
$this->visits = new ArrayCollection();
|
$this->setVisits(new ArrayCollection());
|
||||||
$this->shortCode = '';
|
$this->setShortCode('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
15
src/Exception/InvalidShortCodeException.php
Normal file
15
src/Exception/InvalidShortCodeException.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
namespace Acelaya\UrlShortener\Exception;
|
||||||
|
|
||||||
|
class InvalidShortCodeException extends RuntimeException
|
||||||
|
{
|
||||||
|
public static function fromShortCode($shortCode, $charSet, \Exception $previous = null)
|
||||||
|
{
|
||||||
|
$code = isset($previous) ? $previous->getCode() : -1;
|
||||||
|
return new static(
|
||||||
|
sprintf('Provided short code "%s" does not match the char set "%s"', $shortCode, $charSet),
|
||||||
|
$code,
|
||||||
|
$previous
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
namespace Acelaya\UrlShortener\Service;
|
namespace Acelaya\UrlShortener\Service;
|
||||||
|
|
||||||
use Acelaya\UrlShortener\Entity\ShortUrl;
|
use Acelaya\UrlShortener\Entity\ShortUrl;
|
||||||
|
use Acelaya\UrlShortener\Exception\InvalidShortCodeException;
|
||||||
use Acelaya\UrlShortener\Exception\InvalidUrlException;
|
use Acelaya\UrlShortener\Exception\InvalidUrlException;
|
||||||
use Acelaya\UrlShortener\Exception\RuntimeException;
|
use Acelaya\UrlShortener\Exception\RuntimeException;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
@ -12,7 +13,7 @@ use Psr\Http\Message\UriInterface;
|
|||||||
|
|
||||||
class UrlShortener implements UrlShortenerInterface
|
class UrlShortener implements UrlShortenerInterface
|
||||||
{
|
{
|
||||||
const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ';
|
const DEFAULT_CHARS = 'rYHxLkXfsptbNZzKDG4hy85WFT7BRgMVdC9jvwQPnc6S32Jqm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ClientInterface
|
* @var ClientInterface
|
||||||
@ -38,6 +39,8 @@ class UrlShortener implements UrlShortenerInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Creates and persists a unique shortcode generated for provided url
|
||||||
|
*
|
||||||
* @param UriInterface $url
|
* @param UriInterface $url
|
||||||
* @return string
|
* @return string
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
@ -106,26 +109,37 @@ class UrlShortener implements UrlShortenerInterface
|
|||||||
*/
|
*/
|
||||||
protected function convertAutoincrementIdToShortCode($id)
|
protected function convertAutoincrementIdToShortCode($id)
|
||||||
{
|
{
|
||||||
$id = intval($id);
|
$id = intval($id) + 200000; // Increment the Id so that the generated shortcode is not too short
|
||||||
$length = strlen($this->chars);
|
$length = strlen($this->chars);
|
||||||
$code = '';
|
$code = '';
|
||||||
|
|
||||||
while ($id > $length - 1) {
|
while ($id > 0) {
|
||||||
// Determine the value of the next higher character in the short code and prepend it
|
// Determine the value of the next higher character in the short code and prepend it
|
||||||
$code = $this->chars[fmod($id, $length)] . $code;
|
$code = $this->chars[intval(fmod($id, $length))] . $code;
|
||||||
$id = floor($id / $length);
|
$id = floor($id / $length);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->chars[$id] . $code;
|
return $this->chars[intval($id)] . $code;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Tries to find the mapped URL for provided short code. Returns null if not found
|
||||||
|
*
|
||||||
* @param string $shortCode
|
* @param string $shortCode
|
||||||
* @return string
|
* @return string|null
|
||||||
|
* @throws InvalidShortCodeException
|
||||||
*/
|
*/
|
||||||
public function shortCodeToUrl($shortCode)
|
public function shortCodeToUrl($shortCode)
|
||||||
{
|
{
|
||||||
// Validate short code format
|
// Validate short code format
|
||||||
|
if (! preg_match('|[' . $this->chars . "]+|", $shortCode)) {
|
||||||
|
throw InvalidShortCodeException::fromShortCode($shortCode, $this->chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var ShortUrl $shortUrl */
|
||||||
|
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
||||||
|
'shortCode' => $shortCode,
|
||||||
|
]);
|
||||||
|
return isset($shortUrl) ? $shortUrl->getOriginalUrl() : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Acelaya\UrlShortener\Service;
|
namespace Acelaya\UrlShortener\Service;
|
||||||
|
|
||||||
|
use Acelaya\UrlShortener\Exception\InvalidShortCodeException;
|
||||||
use Acelaya\UrlShortener\Exception\InvalidUrlException;
|
use Acelaya\UrlShortener\Exception\InvalidUrlException;
|
||||||
use Acelaya\UrlShortener\Exception\RuntimeException;
|
use Acelaya\UrlShortener\Exception\RuntimeException;
|
||||||
use Psr\Http\Message\UriInterface;
|
use Psr\Http\Message\UriInterface;
|
||||||
@ -8,6 +9,8 @@ use Psr\Http\Message\UriInterface;
|
|||||||
interface UrlShortenerInterface
|
interface UrlShortenerInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
* Creates and persists a unique shortcode generated for provided url
|
||||||
|
*
|
||||||
* @param UriInterface $url
|
* @param UriInterface $url
|
||||||
* @return string
|
* @return string
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
@ -16,8 +19,11 @@ interface UrlShortenerInterface
|
|||||||
public function urlToShortCode(UriInterface $url);
|
public function urlToShortCode(UriInterface $url);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Tries to find the mapped URL for provided short code. Returns null if not found
|
||||||
|
*
|
||||||
* @param string $shortCode
|
* @param string $shortCode
|
||||||
* @return string
|
* @return string|null
|
||||||
|
* @throws InvalidShortCodeException
|
||||||
*/
|
*/
|
||||||
public function shortCodeToUrl($shortCode);
|
public function shortCodeToUrl($shortCode);
|
||||||
}
|
}
|
||||||
|
13
src/Service/VisitsTrackerInterface.php
Normal file
13
src/Service/VisitsTrackerInterface.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
namespace Acelaya\UrlShortener\Service;
|
||||||
|
|
||||||
|
interface VisitsTrackerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Tracks a new visit to provided short code, using an array of data to look up information
|
||||||
|
*
|
||||||
|
* @param string $shortCode
|
||||||
|
* @param array $visitorData Defaults to global $_SERVER
|
||||||
|
*/
|
||||||
|
public function track($shortCode, array $visitorData = null);
|
||||||
|
}
|
136
tests/Service/UrlShortenerTest.php
Normal file
136
tests/Service/UrlShortenerTest.php
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
namespace AcelayaTest\UrlShortener\Service;
|
||||||
|
|
||||||
|
use Acelaya\UrlShortener\Entity\ShortUrl;
|
||||||
|
use Acelaya\UrlShortener\Service\UrlShortener;
|
||||||
|
use Doctrine\Common\Persistence\ObjectRepository;
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\ORMException;
|
||||||
|
use GuzzleHttp\ClientInterface;
|
||||||
|
use GuzzleHttp\Exception\ClientException;
|
||||||
|
use GuzzleHttp\Psr7\Request;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Zend\Diactoros\Uri;
|
||||||
|
|
||||||
|
class UrlShortenerTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var UrlShortener
|
||||||
|
*/
|
||||||
|
protected $urlShortener;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
protected $em;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
protected $httpClient;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->httpClient = $this->prophesize(ClientInterface::class);
|
||||||
|
|
||||||
|
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$conn = $this->prophesize(Connection::class);
|
||||||
|
$conn->isTransactionActive()->willReturn(false);
|
||||||
|
$this->em->getConnection()->willReturn($conn->reveal());
|
||||||
|
$this->em->flush()->willReturn(null);
|
||||||
|
$this->em->commit()->willReturn(null);
|
||||||
|
$this->em->beginTransaction()->willReturn(null);
|
||||||
|
$this->em->persist(Argument::any())->will(function ($arguments) {
|
||||||
|
/** @var ShortUrl $shortUrl */
|
||||||
|
$shortUrl = $arguments[0];
|
||||||
|
$shortUrl->setId(10);
|
||||||
|
});
|
||||||
|
$repo = $this->prophesize(ObjectRepository::class);
|
||||||
|
$repo->findOneBy(Argument::any())->willReturn(null);
|
||||||
|
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
|
$this->urlShortener = new UrlShortener($this->httpClient->reveal(), $this->em->reveal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function urlIsProperlyShortened()
|
||||||
|
{
|
||||||
|
// 10 -> rY9zc
|
||||||
|
$shortCode = $this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar'));
|
||||||
|
$this->assertEquals('rY9zc', $shortCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @expectedException \Acelaya\UrlShortener\Exception\RuntimeException
|
||||||
|
*/
|
||||||
|
public function exceptionIsThrownWhenOrmThrowsException()
|
||||||
|
{
|
||||||
|
$conn = $this->prophesize(Connection::class);
|
||||||
|
$conn->isTransactionActive()->willReturn(true);
|
||||||
|
$this->em->getConnection()->willReturn($conn->reveal());
|
||||||
|
$this->em->rollback()->shouldBeCalledTimes(1);
|
||||||
|
$this->em->close()->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$this->em->flush()->willThrow(new ORMException());
|
||||||
|
$this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @expectedException \Acelaya\UrlShortener\Exception\InvalidUrlException
|
||||||
|
*/
|
||||||
|
public function exceptionIsThrownWhenUrlDoesNotExist()
|
||||||
|
{
|
||||||
|
$this->httpClient->request(Argument::cetera())->willThrow(
|
||||||
|
new ClientException('', $this->prophesize(Request::class)->reveal())
|
||||||
|
);
|
||||||
|
$this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function whenShortUrlExistsItsShortcodeIsReturned()
|
||||||
|
{
|
||||||
|
$shortUrl = new ShortUrl();
|
||||||
|
$shortUrl->setShortCode('expected_shortcode');
|
||||||
|
$repo = $this->prophesize(ObjectRepository::class);
|
||||||
|
$repo->findOneBy(Argument::any())->willReturn($shortUrl);
|
||||||
|
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
|
$shortCode = $this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar'));
|
||||||
|
$this->assertEquals($shortUrl->getShortCode(), $shortCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function shortCodeIsProperlyParsed()
|
||||||
|
{
|
||||||
|
// rY9zc -> 10
|
||||||
|
$shortUrl = new ShortUrl();
|
||||||
|
$shortUrl->setShortCode('rY9zc')
|
||||||
|
->setOriginalUrl('expected_url');
|
||||||
|
|
||||||
|
$repo = $this->prophesize(ObjectRepository::class);
|
||||||
|
$repo->findOneBy(['shortCode' => 'rY9zc'])->willReturn($shortUrl);
|
||||||
|
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
|
$url = $this->urlShortener->shortCodeToUrl('rY9zc');
|
||||||
|
$this->assertEquals($shortUrl->getOriginalUrl(), $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @expectedException \Acelaya\UrlShortener\Exception\InvalidShortCodeException
|
||||||
|
*/
|
||||||
|
public function invalidCharSetThrowsException()
|
||||||
|
{
|
||||||
|
$this->urlShortener->shortCodeToUrl('&/(');
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user