Implemented UrlShortener main service

This commit is contained in:
Alejandro Celaya 2016-04-17 13:42:52 +02:00
parent 93c2c1298a
commit a9813b1ab9
9 changed files with 198 additions and 42 deletions

View File

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

View File

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

View File

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

View File

@ -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('');
}
/**

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

View File

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

View File

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

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

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