mirror of
https://github.com/shlinkio/shlink.git
synced 2024-12-22 15:13:59 -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",
|
||||
"serve": "php -S 0.0.0.0:8000 -t public/",
|
||||
"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">
|
||||
<testsuites>
|
||||
<testsuite name="App\\Tests">
|
||||
<directory>./test</directory>
|
||||
<testsuite name="AcelayaTest">
|
||||
<directory>./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
|
@ -41,9 +41,9 @@ class ShortUrl extends AbstractEntity
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->dateCreated = new \DateTime();
|
||||
$this->visits = new ArrayCollection();
|
||||
$this->shortCode = '';
|
||||
$this->setDateCreated(new \DateTime());
|
||||
$this->setVisits(new ArrayCollection());
|
||||
$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;
|
||||
|
||||
use Acelaya\UrlShortener\Entity\ShortUrl;
|
||||
use Acelaya\UrlShortener\Exception\InvalidShortCodeException;
|
||||
use Acelaya\UrlShortener\Exception\InvalidUrlException;
|
||||
use Acelaya\UrlShortener\Exception\RuntimeException;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@ -12,7 +13,7 @@ use Psr\Http\Message\UriInterface;
|
||||
|
||||
class UrlShortener implements UrlShortenerInterface
|
||||
{
|
||||
const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ';
|
||||
const DEFAULT_CHARS = 'rYHxLkXfsptbNZzKDG4hy85WFT7BRgMVdC9jvwQPnc6S32Jqm';
|
||||
|
||||
/**
|
||||
* @var ClientInterface
|
||||
@ -38,6 +39,8 @@ class UrlShortener implements UrlShortenerInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and persists a unique shortcode generated for provided url
|
||||
*
|
||||
* @param UriInterface $url
|
||||
* @return string
|
||||
* @throws InvalidUrlException
|
||||
@ -106,26 +109,37 @@ class UrlShortener implements UrlShortenerInterface
|
||||
*/
|
||||
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);
|
||||
$code = '';
|
||||
|
||||
while ($id > $length - 1) {
|
||||
while ($id > 0) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
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
|
||||
* @return string
|
||||
* @return string|null
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
public function shortCodeToUrl($shortCode)
|
||||
{
|
||||
// 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
|
||||
namespace Acelaya\UrlShortener\Service;
|
||||
|
||||
use Acelaya\UrlShortener\Exception\InvalidShortCodeException;
|
||||
use Acelaya\UrlShortener\Exception\InvalidUrlException;
|
||||
use Acelaya\UrlShortener\Exception\RuntimeException;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
@ -8,6 +9,8 @@ use Psr\Http\Message\UriInterface;
|
||||
interface UrlShortenerInterface
|
||||
{
|
||||
/**
|
||||
* Creates and persists a unique shortcode generated for provided url
|
||||
*
|
||||
* @param UriInterface $url
|
||||
* @return string
|
||||
* @throws InvalidUrlException
|
||||
@ -16,8 +19,11 @@ interface UrlShortenerInterface
|
||||
public function urlToShortCode(UriInterface $url);
|
||||
|
||||
/**
|
||||
* Tries to find the mapped URL for provided short code. Returns null if not found
|
||||
*
|
||||
* @param string $shortCode
|
||||
* @return string
|
||||
* @return string|null
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
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