From b436edb99165e5865756f20d97e3b5c95eb3c726 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 17 Apr 2016 10:46:35 +0200 Subject: [PATCH 01/17] Created entities --- composer.json | 2 +- config/autoload/cli-routes.global.php | 15 +++ config/autoload/routes.global.php | 12 +- src/Entity/AbstractEntity.php | 33 ++++++ src/Entity/ShortUrl.php | 118 ++++++++++++++++++++ src/Entity/Visit.php | 155 ++++++++++++++++++++++++++ 6 files changed, 323 insertions(+), 12 deletions(-) create mode 100644 config/autoload/cli-routes.global.php create mode 100644 src/Entity/AbstractEntity.php create mode 100644 src/Entity/ShortUrl.php create mode 100644 src/Entity/Visit.php diff --git a/composer.json b/composer.json index a69b5905..270da115 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "zendframework/zend-expressive-aurarouter": "^1.0", "zendframework/zend-servicemanager": "^3.0", "zendframework/zend-expressive-twigrenderer": "^1.0", - "acelaya/expressive-slim-router": "^2.0" + "doctrine/orm": "^2.5" }, "require-dev": { "phpunit/phpunit": "^4.8", diff --git a/config/autoload/cli-routes.global.php b/config/autoload/cli-routes.global.php new file mode 100644 index 00000000..9a900a7d --- /dev/null +++ b/config/autoload/cli-routes.global.php @@ -0,0 +1,15 @@ + [ + [ + 'name' => 'cli', + 'path' => '/command-name', + 'middleware' => function ($req, $resp) { + + }, + 'allowed_methods' => ['CLI'], + ], + ], + +]; diff --git a/config/autoload/routes.global.php b/config/autoload/routes.global.php index 992f5656..314cbd98 100644 --- a/config/autoload/routes.global.php +++ b/config/autoload/routes.global.php @@ -7,20 +7,10 @@ return [ 'name' => 'home', 'path' => '/', 'middleware' => function ($req, $resp) { - $resp->getBody()->write('Hello world'); - return $resp; + }, 'allowed_methods' => ['GET'], ], - [ - 'name' => 'cli', - 'path' => '/command-name', - 'middleware' => function ($req, $resp) { - $resp->getBody()->write('Hello world from cli'); - return $resp; - }, - 'allowed_methods' => ['CLI'], - ], ], ]; diff --git a/src/Entity/AbstractEntity.php b/src/Entity/AbstractEntity.php new file mode 100644 index 00000000..0fc810ef --- /dev/null +++ b/src/Entity/AbstractEntity.php @@ -0,0 +1,33 @@ +id; + } + + /** + * @param int $id + * @return $this + */ + public function setId($id) + { + $this->id = $id; + return $this; + } +} diff --git a/src/Entity/ShortUrl.php b/src/Entity/ShortUrl.php new file mode 100644 index 00000000..f6091b2b --- /dev/null +++ b/src/Entity/ShortUrl.php @@ -0,0 +1,118 @@ +visits = new ArrayCollection(); + } + + /** + * @return string + */ + public function getOriginalUrl() + { + return $this->originalUrl; + } + + /** + * @param string $originalUrl + * @return $this + */ + public function setOriginalUrl($originalUrl) + { + $this->originalUrl = $originalUrl; + return $this; + } + + /** + * @return string + */ + public function getShortCode() + { + return $this->shortCode; + } + + /** + * @param string $shortCode + * @return $this + */ + public function setShortCode($shortCode) + { + $this->shortCode = $shortCode; + return $this; + } + + /** + * @return \DateTime + */ + public function getDateCreated() + { + return $this->dateCreated; + } + + /** + * @param \DateTime $dateCreated + * @return $this + */ + public function setDateCreated($dateCreated) + { + $this->dateCreated = $dateCreated; + return $this; + } + + /** + * @return Visit[]|Collection + */ + public function getVisits() + { + return $this->visits; + } + + /** + * @param Visit[]|Collection $visits + * @return $this + */ + public function setVisits($visits) + { + $this->visits = $visits; + return $this; + } +} diff --git a/src/Entity/Visit.php b/src/Entity/Visit.php new file mode 100644 index 00000000..01c3ab84 --- /dev/null +++ b/src/Entity/Visit.php @@ -0,0 +1,155 @@ +referer; + } + + /** + * @param string $referer + * @return $this + */ + public function setReferer($referer) + { + $this->referer = $referer; + return $this; + } + + /** + * @return \DateTime + */ + public function getDate() + { + return $this->date; + } + + /** + * @param \DateTime $date + * @return $this + */ + public function setDate($date) + { + $this->date = $date; + return $this; + } + + /** + * @return string + */ + public function getCountry() + { + return $this->country; + } + + /** + * @param string $country + * @return $this + */ + public function setCountry($country) + { + $this->country = $country; + return $this; + } + + /** + * @return string + */ + public function getPlatform() + { + return $this->platform; + } + + /** + * @param string $platform + * @return $this + */ + public function setPlatform($platform) + { + $this->platform = $platform; + return $this; + } + + /** + * @return string + */ + public function getBrowser() + { + return $this->browser; + } + + /** + * @param string $browser + * @return $this + */ + public function setBrowser($browser) + { + $this->browser = $browser; + return $this; + } + + /** + * @return ShortUrl + */ + public function getShortUrl() + { + return $this->shortUrl; + } + + /** + * @param ShortUrl $shortUrl + * @return $this + */ + public function setShortUrl($shortUrl) + { + $this->shortUrl = $shortUrl; + return $this; + } +} From 93c2c1298a09f626c5e9849fc8fc21db6e5db4b5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 17 Apr 2016 11:29:23 +0200 Subject: [PATCH 02/17] Implemented short code generation based on a provided long URL --- composer.json | 3 +- src/Entity/ShortUrl.php | 4 +- src/Exception/ExceptionInterface.php | 6 ++ src/Exception/InvalidUrlException.php | 11 +++ src/Exception/RuntimeException.php | 6 ++ src/Service/UrlShortener.php | 131 ++++++++++++++++++++++++++ src/Service/UrlShortenerInterface.php | 23 +++++ 7 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/InvalidUrlException.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/Service/UrlShortener.php create mode 100644 src/Service/UrlShortenerInterface.php diff --git a/composer.json b/composer.json index 270da115..96fc1489 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "zendframework/zend-expressive-aurarouter": "^1.0", "zendframework/zend-servicemanager": "^3.0", "zendframework/zend-expressive-twigrenderer": "^1.0", - "doctrine/orm": "^2.5" + "doctrine/orm": "^2.5", + "guzzlehttp/guzzle": "^6.2" }, "require-dev": { "phpunit/phpunit": "^4.8", diff --git a/src/Entity/ShortUrl.php b/src/Entity/ShortUrl.php index f6091b2b..cdf7c983 100644 --- a/src/Entity/ShortUrl.php +++ b/src/Entity/ShortUrl.php @@ -41,7 +41,9 @@ class ShortUrl extends AbstractEntity */ public function __construct() { + $this->dateCreated = new \DateTime(); $this->visits = new ArrayCollection(); + $this->shortCode = ''; } /** @@ -58,7 +60,7 @@ class ShortUrl extends AbstractEntity */ public function setOriginalUrl($originalUrl) { - $this->originalUrl = $originalUrl; + $this->originalUrl = (string) $originalUrl; return $this; } diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php new file mode 100644 index 00000000..d6bdc788 --- /dev/null +++ b/src/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ +getCode() : -1; + return new static(sprintf('Provided URL "%s" is not an exisitng and valid URL', $url), $code, $previous); + } +} diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php new file mode 100644 index 00000000..32e61356 --- /dev/null +++ b/src/Exception/RuntimeException.php @@ -0,0 +1,6 @@ +httpClient = $httpClient; + $this->em = $em; + $this->chars = $chars; + } + + /** + * @param UriInterface $url + * @return string + * @throws InvalidUrlException + * @throws RuntimeException + */ + public function urlToShortCode(UriInterface $url) + { + // If the url already exists in the database, just return its short code + $shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([ + 'originalUrl' => $url + ]); + if (isset($shortUrl)) { + return $shortUrl->getShortCode(); + } + + // Check that the URL exists + $this->checkUrlExists($url); + + // Transactionally insert the short url, then generate the short code and finally update the short code + try { + $this->em->beginTransaction(); + + // First, create the short URL with an empty short code + $shortUrl = new ShortUrl(); + $shortUrl->setOriginalUrl($url); + $this->em->persist($shortUrl); + $this->em->flush(); + + // Generate the short code and persist it + $shortCode = $this->convertAutoincrementIdToShortCode($shortUrl->getId()); + $shortUrl->setShortCode($shortCode); + $this->em->flush(); + + $this->em->commit(); + return $shortCode; + } catch (ORMException $e) { + if ($this->em->getConnection()->isTransactionActive()) { + $this->em->rollback(); + $this->em->close(); + } + + throw new RuntimeException('An error occured while persisting the short URL', -1, $e); + } + } + + /** + * Tries to perform a GET request to provided url, returning true on success and false on failure + * + * @param UriInterface $url + * @return bool + */ + protected function checkUrlExists(UriInterface $url) + { + try { + $this->httpClient->request('GET', $url); + } catch (GuzzleException $e) { + throw InvalidUrlException::fromUrl($url, $e); + } + } + + /** + * Generates the unique shortcode for an autoincrement ID + * + * @param int $id + * @return string + */ + protected function convertAutoincrementIdToShortCode($id) + { + $id = intval($id); + $length = strlen($this->chars); + $code = ''; + + while ($id > $length - 1) { + // Determine the value of the next higher character in the short code and prepend it + $code = $this->chars[fmod($id, $length)] . $code; + $id = floor($id / $length); + } + + return $this->chars[$id] . $code; + } + + /** + * @param string $shortCode + * @return string + */ + public function shortCodeToUrl($shortCode) + { + // Validate short code format + + } +} diff --git a/src/Service/UrlShortenerInterface.php b/src/Service/UrlShortenerInterface.php new file mode 100644 index 00000000..6c211c10 --- /dev/null +++ b/src/Service/UrlShortenerInterface.php @@ -0,0 +1,23 @@ + Date: Sun, 17 Apr 2016 13:42:52 +0200 Subject: [PATCH 03/17] Implemented UrlShortener main service --- .travis.yml | 28 ---- composer.json | 2 +- phpunit.xml.dist | 4 +- src/Entity/ShortUrl.php | 6 +- src/Exception/InvalidShortCodeException.php | 15 +++ src/Service/UrlShortener.php | 28 +++- src/Service/UrlShortenerInterface.php | 8 +- src/Service/VisitsTrackerInterface.php | 13 ++ tests/Service/UrlShortenerTest.php | 136 ++++++++++++++++++++ 9 files changed, 198 insertions(+), 42 deletions(-) delete mode 100644 .travis.yml create mode 100644 src/Exception/InvalidShortCodeException.php create mode 100644 src/Service/VisitsTrackerInterface.php create mode 100644 tests/Service/UrlShortenerTest.php diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4d90fc81..00000000 --- a/.travis.yml +++ /dev/null @@ -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 diff --git a/composer.json b/composer.json index 96fc1489..a1b5cf56 100644 --- a/composer.json +++ b/composer.json @@ -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" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index bb69885f..0721b191 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ - - ./test + + ./tests diff --git a/src/Entity/ShortUrl.php b/src/Entity/ShortUrl.php index cdf7c983..994f2e2f 100644 --- a/src/Entity/ShortUrl.php +++ b/src/Entity/ShortUrl.php @@ -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(''); } /** diff --git a/src/Exception/InvalidShortCodeException.php b/src/Exception/InvalidShortCodeException.php new file mode 100644 index 00000000..b41038a1 --- /dev/null +++ b/src/Exception/InvalidShortCodeException.php @@ -0,0 +1,15 @@ +getCode() : -1; + return new static( + sprintf('Provided short code "%s" does not match the char set "%s"', $shortCode, $charSet), + $code, + $previous + ); + } +} diff --git a/src/Service/UrlShortener.php b/src/Service/UrlShortener.php index 0364bf5c..55953691 100644 --- a/src/Service/UrlShortener.php +++ b/src/Service/UrlShortener.php @@ -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; } } diff --git a/src/Service/UrlShortenerInterface.php b/src/Service/UrlShortenerInterface.php index 6c211c10..6623f392 100644 --- a/src/Service/UrlShortenerInterface.php +++ b/src/Service/UrlShortenerInterface.php @@ -1,6 +1,7 @@ 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('&/('); + } +} From db9051dcde7dd8ec22bb33570155dd68f21f5de3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 17 Apr 2016 19:34:16 +0200 Subject: [PATCH 04/17] Added CI config files --- .scrutinizer.yml | 6 +++++ .travis.yml | 26 +++++++++++++++++++ composer.json | 2 +- .../autoload/middleware-pipeline.global.php | 2 +- config/autoload/routes.global.php | 4 +-- 5 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 .scrutinizer.yml create mode 100644 .travis.yml diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 00000000..692b0397 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,6 @@ +tools: + external_code_coverage: true +checks: + php: + code_rating: true + duplication: true diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..77f1332b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +language: php + +branches: + only: + - master + - develop + +php: + - 5.5 + - 5.6 + - 7 + - hhvm + +before_script: + - composer self-update + - composer install --no-interaction + +script: + - mkdir build + - composer check + +after_script: + - wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover build/clover.xml + +sudo: false diff --git a/composer.json b/composer.json index a1b5cf56..486334ef 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,7 @@ "cs": "phpcs", "cs-fix": "phpcbf", "serve": "php -S 0.0.0.0:8000 -t public/", - "test": "phpunit", + "test": "phpunit --coverage-clover build/clover.xml", "pretty-test": "phpunit --coverage-html build/coverage" } } diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index 9f5f2d63..8a393ce1 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -3,7 +3,7 @@ use Zend\Expressive\Container\ApplicationFactory; use Zend\Expressive\Helper; return [ - + 'middleware_pipeline' => [ 'always' => [ 'middleware' => [ diff --git a/config/autoload/routes.global.php b/config/autoload/routes.global.php index 314cbd98..48522662 100644 --- a/config/autoload/routes.global.php +++ b/config/autoload/routes.global.php @@ -7,10 +7,10 @@ return [ 'name' => 'home', 'path' => '/', 'middleware' => function ($req, $resp) { - + }, 'allowed_methods' => ['GET'], ], ], - + ]; From 03298fc4482bb0b2e74916a6fd8131b25e9d9d59 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 17 Apr 2016 20:27:24 +0200 Subject: [PATCH 05/17] Created EntityManagerFactory --- bin/cli | 5 ++- cli-config.php | 11 ++++++ composer.json | 3 +- config/autoload/database.local.php.dist | 15 ++++++++ config/autoload/services.global.php | 23 ++++++++++-- config/{app.php => container.php} | 5 +-- data/{ => cache}/.gitignore | 0 data/proxies/.gitignore | 2 + public/index.php | 5 ++- src/Factory/EntityManagerFactory.php | 43 ++++++++++++++++++++++ src/Service/UrlShortener.php | 9 +++++ tests/Factory/EntityManagerFactoryTest.php | 38 +++++++++++++++++++ 12 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 cli-config.php create mode 100644 config/autoload/database.local.php.dist rename config/{app.php => container.php} (74%) rename data/{ => cache}/.gitignore (100%) create mode 100644 data/proxies/.gitignore create mode 100644 src/Factory/EntityManagerFactory.php create mode 100644 tests/Factory/EntityManagerFactoryTest.php diff --git a/bin/cli b/bin/cli index 1aa55f63..0aca7dd0 100644 --- a/bin/cli +++ b/bin/cli @@ -1,11 +1,14 @@ #!/usr/bin/env php get(Application::class); $command = count($_SERVER['argv']) > 1 ? $_SERVER['argv'][1] : ''; $request = ServerRequestFactory::fromGlobals() diff --git a/cli-config.php b/cli-config.php new file mode 100644 index 00000000..be9b8b82 --- /dev/null +++ b/cli-config.php @@ -0,0 +1,11 @@ +get(EntityManager::class); + +return ConsoleRunner::createHelperSet($em); diff --git a/composer.json b/composer.json index 486334ef..1d9e2ce0 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "zendframework/zend-servicemanager": "^3.0", "zendframework/zend-expressive-twigrenderer": "^1.0", "doctrine/orm": "^2.5", - "guzzlehttp/guzzle": "^6.2" + "guzzlehttp/guzzle": "^6.2", + "acelaya/zsm-annotated-services": "^0.2.0" }, "require-dev": { "phpunit/phpunit": "^4.8", diff --git a/config/autoload/database.local.php.dist b/config/autoload/database.local.php.dist new file mode 100644 index 00000000..f90c4a2e --- /dev/null +++ b/config/autoload/database.local.php.dist @@ -0,0 +1,15 @@ + [ + 'driver' => 'pdo_mysql', + 'user' => '', + 'password' => '', + 'dbname' => '', + 'charset' => 'utf-8', + 'driverOptions' => [ + PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8' + ], + ] + +]; diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 8f2a8816..f3ac64bd 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -1,16 +1,22 @@ [ 'invokables' => [ - Helper\ServerUrlHelper::class => Helper\ServerUrlHelper::class, - Router\RouterInterface::class => Router\AuraRouter::class, + Helper\ServerUrlHelper::class, + Router\AuraRouter::class, ], 'factories' => [ Application::class => Container\ApplicationFactory::class, @@ -19,11 +25,22 @@ return [ Helper\UrlHelper::class => Helper\UrlHelperFactory::class, Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class, Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class, + Helper\ServerUrlHelper::class => InvokableFactory::class, + Router\RouterInterface::class => InvokableFactory::class, // View 'Zend\Expressive\FinalHandler' => Container\TemplatedErrorHandlerFactory::class, - Template\TemplateRendererInterface::class => Zend\Expressive\Twig\TwigRendererFactory::class, + Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class, + + // Services + EntityManager::class => EntityManagerFactory::class, + GuzzleHttp\Client::class => InvokableFactory::class, + UrlShortener::class => AnnotatedFactory::class, ], + 'aliases' => [ + 'em' => EntityManager::class, + 'httpClient' => GuzzleHttp\Client::class, + ] ], ]; diff --git a/config/app.php b/config/container.php similarity index 74% rename from config/app.php rename to config/container.php index 08b73799..332023f8 100644 --- a/config/app.php +++ b/config/container.php @@ -4,11 +4,10 @@ use Zend\ServiceManager\ServiceManager; chdir(dirname(__DIR__)); -require __DIR__ . '/../vendor/autoload.php'; +require 'vendor/autoload.php'; // Build container $config = require __DIR__ . '/config.php'; $container = new ServiceManager($config['services']); $container->setService('config', $config); - -return $container->get(Application::class); +return $container; diff --git a/data/.gitignore b/data/cache/.gitignore similarity index 100% rename from data/.gitignore rename to data/cache/.gitignore diff --git a/data/proxies/.gitignore b/data/proxies/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/data/proxies/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/public/index.php b/public/index.php index 16af56e5..6b310943 100644 --- a/public/index.php +++ b/public/index.php @@ -1,6 +1,9 @@ get(Application::class); $app->run(); diff --git a/src/Factory/EntityManagerFactory.php b/src/Factory/EntityManagerFactory.php new file mode 100644 index 00000000..8716b3f8 --- /dev/null +++ b/src/Factory/EntityManagerFactory.php @@ -0,0 +1,43 @@ +get('config'); + $isDevMode = isset($globalConfig['debug']) ? ((bool) $globalConfig['debug']) : false; + $cache = $container->has(Cache::class) ? $container->get(Cache::class) : new ArrayCache(); + $dbConfig = isset($globalConfig['database']) ? $globalConfig['database'] : []; + + return EntityManager::create($dbConfig, Setup::createAnnotationMetadataConfiguration( + ['src/Entity'], + $isDevMode, + 'data/proxies', + $cache, + false + )); + } +} diff --git a/src/Service/UrlShortener.php b/src/Service/UrlShortener.php index 55953691..3fee0cf3 100644 --- a/src/Service/UrlShortener.php +++ b/src/Service/UrlShortener.php @@ -5,6 +5,7 @@ use Acelaya\UrlShortener\Entity\ShortUrl; use Acelaya\UrlShortener\Exception\InvalidShortCodeException; use Acelaya\UrlShortener\Exception\InvalidUrlException; use Acelaya\UrlShortener\Exception\RuntimeException; +use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\ORMException; use GuzzleHttp\ClientInterface; @@ -28,6 +29,14 @@ class UrlShortener implements UrlShortenerInterface */ private $chars; + /** + * UrlShortener constructor. + * @param ClientInterface $httpClient + * @param EntityManagerInterface $em + * @param string $chars + * + * @Inject({"httpClient", "em"}) + */ public function __construct( ClientInterface $httpClient, EntityManagerInterface $em, diff --git a/tests/Factory/EntityManagerFactoryTest.php b/tests/Factory/EntityManagerFactoryTest.php new file mode 100644 index 00000000..4b9bd82f --- /dev/null +++ b/tests/Factory/EntityManagerFactoryTest.php @@ -0,0 +1,38 @@ +factory = new EntityManagerFactory(); + } + + /** + * @test + */ + public function serviceIsCreated() + { + $sm = new ServiceManager(['services' => [ + 'config' => [ + 'debug' => true, + 'database' => [ + 'driver' => 'pdo_sqlite', + ], + ], + ]]); + + $em = $this->factory->__invoke($sm, EntityManager::class); + $this->assertInstanceOf(EntityManager::class, $em); + } +} From a60a6ccc4d70cb6b27bdccaa2c6c71bdc77a8413 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Apr 2016 17:21:35 +0200 Subject: [PATCH 06/17] Added dotenv component to define env vars in local file --- .env.dist | 7 ++++++ .gitignore | 1 + composer.json | 3 ++- config/autoload/database.global.php | 15 +++++++++++++ config/autoload/database.local.php.dist | 15 ------------- config/autoload/services.global.php | 10 ++++----- config/container.php | 10 ++++++++- src/Factory/CacheFactory.php | 30 +++++++++++++++++++++++++ 8 files changed, 69 insertions(+), 22 deletions(-) create mode 100644 .env.dist create mode 100644 config/autoload/database.global.php delete mode 100644 config/autoload/database.local.php.dist create mode 100644 src/Factory/CacheFactory.php diff --git a/.env.dist b/.env.dist new file mode 100644 index 00000000..388e7e1b --- /dev/null +++ b/.env.dist @@ -0,0 +1,7 @@ +# Application +APP_ENV= + +# Database +DB_USER= +DB_PASSWORD= +DB_NAME= diff --git a/.gitignore b/.gitignore index 397e34a5..5aedee0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build composer.lock vendor/ +.env diff --git a/composer.json b/composer.json index 1d9e2ce0..652d81d1 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,8 @@ "squizlabs/php_codesniffer": "^2.3", "roave/security-advisories": "dev-master", "filp/whoops": "^2.0", - "symfony/var-dumper": "^3.0" + "symfony/var-dumper": "^3.0", + "vlucas/phpdotenv": "^2.2" }, "autoload": { "psr-4": { diff --git a/config/autoload/database.global.php b/config/autoload/database.global.php new file mode 100644 index 00000000..4e3b00e3 --- /dev/null +++ b/config/autoload/database.global.php @@ -0,0 +1,15 @@ + [ + 'driver' => 'pdo_mysql', + 'user' => getenv('DB_USER'), + 'password' => getenv('DB_PASSWORD'), + 'dbname' => getenv('DB_NAME') ?: 'acelaya_url_shortener', + 'charset' => 'utf8', + 'driverOptions' => [ + PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8' + ], + ], + +]; diff --git a/config/autoload/database.local.php.dist b/config/autoload/database.local.php.dist deleted file mode 100644 index f90c4a2e..00000000 --- a/config/autoload/database.local.php.dist +++ /dev/null @@ -1,15 +0,0 @@ - [ - 'driver' => 'pdo_mysql', - 'user' => '', - 'password' => '', - 'dbname' => '', - 'charset' => 'utf-8', - 'driverOptions' => [ - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8' - ], - ] - -]; diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index f3ac64bd..468c4a75 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -1,7 +1,9 @@ [ - 'invokables' => [ - Helper\ServerUrlHelper::class, - Router\AuraRouter::class, - ], 'factories' => [ Application::class => Container\ApplicationFactory::class, - // Url helpers + // Routes Helper\UrlHelper::class => Helper\UrlHelperFactory::class, Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class, Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class, Helper\ServerUrlHelper::class => InvokableFactory::class, Router\RouterInterface::class => InvokableFactory::class, + Router\AuraRouter::class => InvokableFactory::class, // View 'Zend\Expressive\FinalHandler' => Container\TemplatedErrorHandlerFactory::class, @@ -36,6 +35,7 @@ return [ EntityManager::class => EntityManagerFactory::class, GuzzleHttp\Client::class => InvokableFactory::class, UrlShortener::class => AnnotatedFactory::class, + Cache::class => CacheFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/config/container.php b/config/container.php index 332023f8..5678edcb 100644 --- a/config/container.php +++ b/config/container.php @@ -1,11 +1,19 @@ load(); +} + // Build container $config = require __DIR__ . '/config.php'; $container = new ServiceManager($config['services']); diff --git a/src/Factory/CacheFactory.php b/src/Factory/CacheFactory.php new file mode 100644 index 00000000..86c88e6b --- /dev/null +++ b/src/Factory/CacheFactory.php @@ -0,0 +1,30 @@ + Date: Sat, 30 Apr 2016 17:47:48 +0200 Subject: [PATCH 07/17] Everything configured to generate routes from CLI --- .env.dist | 2 ++ config/autoload/cli-routes.global.php | 10 +++++----- config/autoload/routes.global.php | 14 ++++++-------- config/autoload/services.global.php | 8 ++++++-- config/autoload/url-shortener.global.php | 9 +++++++++ config/config.php | 4 +--- src/Entity/ShortUrl.php | 2 +- 7 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 config/autoload/url-shortener.global.php diff --git a/.env.dist b/.env.dist index 388e7e1b..2e34016a 100644 --- a/.env.dist +++ b/.env.dist @@ -1,5 +1,7 @@ # Application APP_ENV= +SHORTENED_URL_SCHEMA= +SHORTENED_URL_HOSTNAME= # Database DB_USER= diff --git a/config/autoload/cli-routes.global.php b/config/autoload/cli-routes.global.php index 9a900a7d..c2aafdf1 100644 --- a/config/autoload/cli-routes.global.php +++ b/config/autoload/cli-routes.global.php @@ -1,13 +1,13 @@ [ [ - 'name' => 'cli', - 'path' => '/command-name', - 'middleware' => function ($req, $resp) { - - }, + 'name' => 'cli-generate-shortcode', + 'path' => '/generate-shortcode', + 'middleware' => CliRoutable\GenerateShortcodeMiddleware::class, 'allowed_methods' => ['CLI'], ], ], diff --git a/config/autoload/routes.global.php b/config/autoload/routes.global.php index 48522662..a95711aa 100644 --- a/config/autoload/routes.global.php +++ b/config/autoload/routes.global.php @@ -3,14 +3,12 @@ return [ 'routes' => [ - [ - 'name' => 'home', - 'path' => '/', - 'middleware' => function ($req, $resp) { - - }, - 'allowed_methods' => ['GET'], - ], +// [ +// 'name' => 'home', +// 'path' => '/', +// 'middleware' => '', +// 'allowed_methods' => ['GET'], +// ], ], ]; diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 468c4a75..5d47160a 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -1,6 +1,7 @@ [ Application::class => Container\ApplicationFactory::class, - // Routes + // Url helpers Helper\UrlHelper::class => Helper\UrlHelperFactory::class, Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class, Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class, Helper\ServerUrlHelper::class => InvokableFactory::class, - Router\RouterInterface::class => InvokableFactory::class, Router\AuraRouter::class => InvokableFactory::class, // View @@ -36,10 +36,14 @@ return [ GuzzleHttp\Client::class => InvokableFactory::class, UrlShortener::class => AnnotatedFactory::class, Cache::class => CacheFactory::class, + + // Middleware + Middleware\CliRoutable\GenerateShortcodeMiddleware::class => AnnotatedFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, 'httpClient' => GuzzleHttp\Client::class, + Router\RouterInterface::class => Router\AuraRouter::class, ] ], diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php new file mode 100644 index 00000000..795ce021 --- /dev/null +++ b/config/autoload/url-shortener.global.php @@ -0,0 +1,9 @@ + [ + 'schema' => getenv('SHORTENED_URL_SCHEMA') ?: 'http', + 'hostname' => getenv('SHORTENED_URL_HOSTNAME'), + ], + +]; diff --git a/config/config.php b/config/config.php index 4838f371..a3d0e7ac 100644 --- a/config/config.php +++ b/config/config.php @@ -29,6 +29,4 @@ if (is_file($cachedConfigFile)) { } } -// Return an ArrayObject so we can inject the config as a service in Aura.Di -// and still use array checks like ``is_array``. -return new ArrayObject($config, ArrayObject::ARRAY_AS_PROPS); +return $config; diff --git a/src/Entity/ShortUrl.php b/src/Entity/ShortUrl.php index 994f2e2f..35c7ed1b 100644 --- a/src/Entity/ShortUrl.php +++ b/src/Entity/ShortUrl.php @@ -27,7 +27,7 @@ class ShortUrl extends AbstractEntity protected $shortCode; /** * @var \DateTime - * @ORM\Column(name="date_created", ) + * @ORM\Column(name="date_created", type="datetime") */ protected $dateCreated; /** From 577ad146a4623afe8cdf435bfde125f378bae4a7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Apr 2016 18:14:43 +0200 Subject: [PATCH 08/17] Implemented shortcode generation --- .../autoload/middleware-pipeline.global.php | 2 + config/autoload/services.global.php | 1 + src/Middleware/CliParamsMiddleware.php | 66 ++++++++++++++ .../GenerateShortcodeMiddleware.php | 91 +++++++++++++++++++ .../Factory/CliParamsMiddlewareFactory.php | 29 ++++++ 5 files changed, 189 insertions(+) create mode 100644 src/Middleware/CliParamsMiddleware.php create mode 100644 src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php create mode 100644 src/Middleware/Factory/CliParamsMiddlewareFactory.php diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index 8a393ce1..d033f9b2 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -1,4 +1,5 @@ [ 'middleware' => [ ApplicationFactory::ROUTING_MIDDLEWARE, + CliParamsMiddleware::class, Helper\UrlHelperMiddleware::class, ApplicationFactory::DISPATCH_MIDDLEWARE, ], diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 5d47160a..421c8a00 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -39,6 +39,7 @@ return [ // Middleware Middleware\CliRoutable\GenerateShortcodeMiddleware::class => AnnotatedFactory::class, + Middleware\CliParamsMiddleware::class => Middleware\Factory\CliParamsMiddlewareFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/src/Middleware/CliParamsMiddleware.php b/src/Middleware/CliParamsMiddleware.php new file mode 100644 index 00000000..c750ff9b --- /dev/null +++ b/src/Middleware/CliParamsMiddleware.php @@ -0,0 +1,66 @@ +argv = $argv; + } + + /** + * Process an incoming request and/or response. + * + * Accepts a server-side request and a response instance, and does + * something with them. + * + * If the response is not complete and/or further processing would not + * interfere with the work done in the middleware, or if the middleware + * wants to delegate to another process, it can use the `$out` callable + * if present. + * + * If the middleware does not return a value, execution of the current + * request is considered complete, and the response instance provided will + * be considered the response to return. + * + * Alternately, the middleware may return a response instance. + * + * Often, middleware will `return $out();`, with the assumption that a + * later middleware will return a response. + * + * @param Request $request + * @param Response $response + * @param null|callable $out + * @return null|Response + */ + public function __invoke(Request $request, Response $response, callable $out = null) + { + // When not in CLI, just call next middleware + if (! php_sapi_name() === 'cli') { + return $out($request, $response); + } + + /** @var RouteResult $routeResult */ + $routeResult = $request->getAttribute(RouteResult::class); + if (! $routeResult->isSuccess()) { + return $out($request, $response); + } + + // Inject ARGV params as request attributes + if ($routeResult->getMatchedRouteName() === 'cli-generate-shortcode') { + $request = $request->withAttribute('longUrl', isset($this->argv[2]) ? $this->argv[2] : null); + } + + return $out($request, $response); + } +} diff --git a/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php b/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php new file mode 100644 index 00000000..44fa3bf8 --- /dev/null +++ b/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php @@ -0,0 +1,91 @@ +urlShortener = $urlShortener; + $this->config = $config; + } + + /** + * Process an incoming request and/or response. + * + * Accepts a server-side request and a response instance, and does + * something with them. + * + * If the response is not complete and/or further processing would not + * interfere with the work done in the middleware, or if the middleware + * wants to delegate to another process, it can use the `$out` callable + * if present. + * + * If the middleware does not return a value, execution of the current + * request is considered complete, and the response instance provided will + * be considered the response to return. + * + * Alternately, the middleware may return a response instance. + * + * Often, middleware will `return $out();`, with the assumption that a + * later middleware will return a response. + * + * @param Request $request + * @param Response $response + * @param null|callable $out + * @return null|Response + */ + public function __invoke(Request $request, Response $response, callable $out = null) + { + $longUrl = $request->getAttribute('longUrl'); + + try { + if (! isset($longUrl)) { + $response->getBody()->write('A URL was not provided!' . PHP_EOL); + return; + } + + $shortcode = $this->urlShortener->urlToShortCode(new Uri($longUrl)); + $shortUrl = (new Uri())->withPath($shortcode) + ->withScheme($this->config['schema']) + ->withHost($this->config['hostname']); + + $response->getBody()->write( + sprintf('Processed URL "%s".%sGenerated short URL "%s"', $longUrl, PHP_EOL, $shortUrl) . PHP_EOL + ); + } catch (InvalidUrlException $e) { + $response->getBody()->write( + sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl) . PHP_EOL + ); + } catch (\Exception $e) { + $response->getBody()->write($e); + } finally { + return is_callable($out) ? $out($request, $response) : $response; + } + } +} diff --git a/src/Middleware/Factory/CliParamsMiddlewareFactory.php b/src/Middleware/Factory/CliParamsMiddlewareFactory.php new file mode 100644 index 00000000..73e365ac --- /dev/null +++ b/src/Middleware/Factory/CliParamsMiddlewareFactory.php @@ -0,0 +1,29 @@ + Date: Sat, 30 Apr 2016 18:59:03 +0200 Subject: [PATCH 09/17] Implemented routable redirect middleware --- composer.json | 6 +- config/autoload/routes.global.php | 13 ++-- config/autoload/services.global.php | 5 +- src/Middleware/CliParamsMiddleware.php | 2 +- .../Factory/CliParamsMiddlewareFactory.php | 2 +- .../Routable/RedirectMiddleware.php | 75 +++++++++++++++++++ templates/error/404.html.twig | 3 +- templates/layout/default.html.twig | 36 +-------- 8 files changed, 92 insertions(+), 50 deletions(-) create mode 100644 src/Middleware/Routable/RedirectMiddleware.php diff --git a/composer.json b/composer.json index 652d81d1..d43d7fbb 100644 --- a/composer.json +++ b/composer.json @@ -14,10 +14,10 @@ "php": "^5.5 || ^7.0", "zendframework/zend-expressive": "^1.0", "zendframework/zend-expressive-helpers": "^2.0", - "zendframework/zend-stdlib": "^2.7", - "zendframework/zend-expressive-aurarouter": "^1.0", - "zendframework/zend-servicemanager": "^3.0", + "zendframework/zend-expressive-fastroute": "^1.1", "zendframework/zend-expressive-twigrenderer": "^1.0", + "zendframework/zend-stdlib": "^2.7", + "zendframework/zend-servicemanager": "^3.0", "doctrine/orm": "^2.5", "guzzlehttp/guzzle": "^6.2", "acelaya/zsm-annotated-services": "^0.2.0" diff --git a/config/autoload/routes.global.php b/config/autoload/routes.global.php index a95711aa..40a3d20b 100644 --- a/config/autoload/routes.global.php +++ b/config/autoload/routes.global.php @@ -1,14 +1,15 @@ [ -// [ -// 'name' => 'home', -// 'path' => '/', -// 'middleware' => '', -// 'allowed_methods' => ['GET'], -// ], + [ + 'name' => 'long-url-redirect', + 'path' => '/{shortCode}', + 'middleware' => Routable\RedirectMiddleware::class, + 'allowed_methods' => ['GET'], + ], ], ]; diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 421c8a00..414b2e56 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -25,7 +25,7 @@ return [ Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class, Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class, Helper\ServerUrlHelper::class => InvokableFactory::class, - Router\AuraRouter::class => InvokableFactory::class, + Router\FastRouteRouter::class => InvokableFactory::class, // View 'Zend\Expressive\FinalHandler' => Container\TemplatedErrorHandlerFactory::class, @@ -39,12 +39,13 @@ return [ // Middleware Middleware\CliRoutable\GenerateShortcodeMiddleware::class => AnnotatedFactory::class, + Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class, Middleware\CliParamsMiddleware::class => Middleware\Factory\CliParamsMiddlewareFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, 'httpClient' => GuzzleHttp\Client::class, - Router\RouterInterface::class => Router\AuraRouter::class, + Router\RouterInterface::class => Router\FastRouteRouter::class, ] ], diff --git a/src/Middleware/CliParamsMiddleware.php b/src/Middleware/CliParamsMiddleware.php index c750ff9b..da243a15 100644 --- a/src/Middleware/CliParamsMiddleware.php +++ b/src/Middleware/CliParamsMiddleware.php @@ -46,7 +46,7 @@ class CliParamsMiddleware implements MiddlewareInterface public function __invoke(Request $request, Response $response, callable $out = null) { // When not in CLI, just call next middleware - if (! php_sapi_name() === 'cli') { + if (php_sapi_name() !== 'cli') { return $out($request, $response); } diff --git a/src/Middleware/Factory/CliParamsMiddlewareFactory.php b/src/Middleware/Factory/CliParamsMiddlewareFactory.php index 73e365ac..b806cbeb 100644 --- a/src/Middleware/Factory/CliParamsMiddlewareFactory.php +++ b/src/Middleware/Factory/CliParamsMiddlewareFactory.php @@ -24,6 +24,6 @@ class CliParamsMiddlewareFactory implements FactoryInterface */ public function __invoke(ContainerInterface $container, $requestedName, array $options = null) { - return new CliParamsMiddleware($_SERVER['argv']); + return new CliParamsMiddleware(isset($_SERVER['argv']) ? $_SERVER['argv'] : []); } } diff --git a/src/Middleware/Routable/RedirectMiddleware.php b/src/Middleware/Routable/RedirectMiddleware.php new file mode 100644 index 00000000..29870d46 --- /dev/null +++ b/src/Middleware/Routable/RedirectMiddleware.php @@ -0,0 +1,75 @@ +urlShortener = $urlShortener; + } + + /** + * Process an incoming request and/or response. + * + * Accepts a server-side request and a response instance, and does + * something with them. + * + * If the response is not complete and/or further processing would not + * interfere with the work done in the middleware, or if the middleware + * wants to delegate to another process, it can use the `$out` callable + * if present. + * + * If the middleware does not return a value, execution of the current + * request is considered complete, and the response instance provided will + * be considered the response to return. + * + * Alternately, the middleware may return a response instance. + * + * Often, middleware will `return $out();`, with the assumption that a + * later middleware will return a response. + * + * @param Request $request + * @param Response $response + * @param null|callable $out + * @return null|Response + */ + public function __invoke(Request $request, Response $response, callable $out = null) + { + $shortCode = $request->getAttribute('shortCode', ''); + + try { + $longUrl = $this->urlShortener->shortCodeToUrl($shortCode); + + // If provided shortCode does not belong to a valid long URL, dispatch next middleware, which is 404 + // middleware + if (! isset($longUrl)) { + return $out($request, $response); + } + + // Return a redirect response to the long URL + return new RedirectResponse($longUrl, 301); + } catch (\Exception $e) { + // In case of error, dispatch 404 error + return $out($request, $response); + } + } +} diff --git a/templates/error/404.html.twig b/templates/error/404.html.twig index 7b4e363e..8e3f3a7a 100644 --- a/templates/error/404.html.twig +++ b/templates/error/404.html.twig @@ -7,7 +7,6 @@

This is awkward.

We encountered a 404 Not Found error.

- You are looking for something that doesn't exist or may have moved. Check out one of the links on this page - or head back to Home. + You are looking for something that doesn't exist or may have moved.

{% endblock %} diff --git a/templates/layout/default.html.twig b/templates/layout/default.html.twig index b3664d0e..ac34990a 100644 --- a/templates/layout/default.html.twig +++ b/templates/layout/default.html.twig @@ -17,40 +17,6 @@ {% block stylesheets %}{% endblock %} -
- -
-
{% block content %}{% endblock %} @@ -62,7 +28,7 @@
{% block footer %}

- © 2005 - {{ "now"|date("Y") }} by Zend Technologies Ltd. All rights reserved. + © {{ "now" | date("Y") }} by Alejandro Celaya.

{% endblock %}
From 4ae08c02ec05b0fb30dff7bfc8ffdde4c72e7e6b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Apr 2016 19:18:42 +0200 Subject: [PATCH 10/17] Created VisitsTracker service to track visits to shortcodes --- config/autoload/services.global.php | 5 +- src/Entity/Visit.php | 110 ++++++++---------- .../Routable/RedirectMiddleware.php | 22 +++- src/Service/VisitsTracker.php | 61 ++++++++++ 4 files changed, 127 insertions(+), 71 deletions(-) create mode 100644 src/Service/VisitsTracker.php diff --git a/config/autoload/services.global.php b/config/autoload/services.global.php index 414b2e56..300f1bf3 100644 --- a/config/autoload/services.global.php +++ b/config/autoload/services.global.php @@ -2,7 +2,7 @@ use Acelaya\UrlShortener\Factory\CacheFactory; use Acelaya\UrlShortener\Factory\EntityManagerFactory; use Acelaya\UrlShortener\Middleware; -use Acelaya\UrlShortener\Service\UrlShortener; +use Acelaya\UrlShortener\Service; use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory; use Doctrine\Common\Cache\Cache; use Doctrine\ORM\EntityManager; @@ -34,7 +34,8 @@ return [ // Services EntityManager::class => EntityManagerFactory::class, GuzzleHttp\Client::class => InvokableFactory::class, - UrlShortener::class => AnnotatedFactory::class, + Service\UrlShortener::class => AnnotatedFactory::class, + Service\VisitsTracker::class => AnnotatedFactory::class, Cache::class => CacheFactory::class, // Middleware diff --git a/src/Entity/Visit.php b/src/Entity/Visit.php index 01c3ab84..2e8fad6f 100644 --- a/src/Entity/Visit.php +++ b/src/Entity/Visit.php @@ -15,7 +15,7 @@ class Visit extends AbstractEntity { /** * @var string - * @ORM\Column(type="string", length=256) + * @ORM\Column(type="string", length=256, nullable=true) */ protected $referer; /** @@ -25,19 +25,14 @@ class Visit extends AbstractEntity protected $date; /** * @var string - * @ORM\Column(type="string", length=256) + * @ORM\Column(type="string", length=256, name="remote_addr", nullable=true) */ - protected $country; + protected $remoteAddr; /** * @var string - * @ORM\Column(type="string", length=256) + * @ORM\Column(type="string", length=256, name="user_agent", nullable=true) */ - protected $platform; - /** - * @var string - * @ORM\Column(type="string", length=256) - */ - protected $browser; + protected $userAgent; /** * @var ShortUrl * @ORM\ManyToOne(targetEntity=ShortUrl::class) @@ -45,6 +40,11 @@ class Visit extends AbstractEntity */ protected $shortUrl; + public function __construct() + { + $this->date = new \DateTime(); + } + /** * @return string */ @@ -81,60 +81,6 @@ class Visit extends AbstractEntity return $this; } - /** - * @return string - */ - public function getCountry() - { - return $this->country; - } - - /** - * @param string $country - * @return $this - */ - public function setCountry($country) - { - $this->country = $country; - return $this; - } - - /** - * @return string - */ - public function getPlatform() - { - return $this->platform; - } - - /** - * @param string $platform - * @return $this - */ - public function setPlatform($platform) - { - $this->platform = $platform; - return $this; - } - - /** - * @return string - */ - public function getBrowser() - { - return $this->browser; - } - - /** - * @param string $browser - * @return $this - */ - public function setBrowser($browser) - { - $this->browser = $browser; - return $this; - } - /** * @return ShortUrl */ @@ -152,4 +98,40 @@ class Visit extends AbstractEntity $this->shortUrl = $shortUrl; return $this; } + + /** + * @return string + */ + public function getRemoteAddr() + { + return $this->remoteAddr; + } + + /** + * @param string $remoteAddr + * @return $this + */ + public function setRemoteAddr($remoteAddr) + { + $this->remoteAddr = $remoteAddr; + return $this; + } + + /** + * @return string + */ + public function getUserAgent() + { + return $this->userAgent; + } + + /** + * @param string $userAgent + * @return $this + */ + public function setUserAgent($userAgent) + { + $this->userAgent = $userAgent; + return $this; + } } diff --git a/src/Middleware/Routable/RedirectMiddleware.php b/src/Middleware/Routable/RedirectMiddleware.php index 29870d46..104acd44 100644 --- a/src/Middleware/Routable/RedirectMiddleware.php +++ b/src/Middleware/Routable/RedirectMiddleware.php @@ -3,6 +3,8 @@ namespace Acelaya\UrlShortener\Middleware\Routable; use Acelaya\UrlShortener\Service\UrlShortener; use Acelaya\UrlShortener\Service\UrlShortenerInterface; +use Acelaya\UrlShortener\Service\VisitsTracker; +use Acelaya\UrlShortener\Service\VisitsTrackerInterface; use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; @@ -15,16 +17,22 @@ class RedirectMiddleware implements MiddlewareInterface * @var UrlShortenerInterface */ private $urlShortener; + /** + * @var VisitsTracker|VisitsTrackerInterface + */ + private $visitTracker; /** * RedirectMiddleware constructor. * @param UrlShortenerInterface|UrlShortener $urlShortener + * @param VisitsTrackerInterface|VisitsTracker $visitTracker * - * @Inject({UrlShortener::class}) + * @Inject({UrlShortener::class, VisitsTracker::class}) */ - public function __construct(UrlShortenerInterface $urlShortener) + public function __construct(UrlShortenerInterface $urlShortener, VisitsTrackerInterface $visitTracker) { $this->urlShortener = $urlShortener; + $this->visitTracker = $visitTracker; } /** @@ -55,7 +63,7 @@ class RedirectMiddleware implements MiddlewareInterface public function __invoke(Request $request, Response $response, callable $out = null) { $shortCode = $request->getAttribute('shortCode', ''); - + try { $longUrl = $this->urlShortener->shortCodeToUrl($shortCode); @@ -65,8 +73,12 @@ class RedirectMiddleware implements MiddlewareInterface return $out($request, $response); } - // Return a redirect response to the long URL - return new RedirectResponse($longUrl, 301); + // Track visit to this shortcode + $this->visitTracker->track($shortCode); + + // Return a redirect response to the long URL. + // Use a temporary redirect to make sure browsers aleways hit the server for analytics purposes + return new RedirectResponse($longUrl); } catch (\Exception $e) { // In case of error, dispatch 404 error return $out($request, $response); diff --git a/src/Service/VisitsTracker.php b/src/Service/VisitsTracker.php new file mode 100644 index 00000000..80598053 --- /dev/null +++ b/src/Service/VisitsTracker.php @@ -0,0 +1,61 @@ +em = $em; + } + + /** + * 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) + { + $visitorData = $visitorData ?: $_SERVER; + + /** @var ShortUrl $shortUrl */ + $shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([ + 'shortCode' => $shortCode, + ]); + + $visit = new Visit(); + $visit->setShortUrl($shortUrl) + ->setUserAgent($this->getArrayValue($visitorData, 'HTTP_USER_AGENT')) + ->setReferer($this->getArrayValue($visitorData, 'REFERER')) + ->setRemoteAddr($this->getArrayValue($visitorData, 'REMOTE_ADDR')); + $this->em->persist($visit); + $this->em->flush(); + } + + /** + * @param array $array + * @param $key + * @param null $default + * @return mixed|null + */ + protected function getArrayValue(array $array, $key, $default = null) + { + return isset($array[$key]) ? $array[$key] : $default; + } +} From 3bb899bcedd5ea37117eaeee771fd9b1e63d8725 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Apr 2016 19:24:45 +0200 Subject: [PATCH 11/17] Prevented routable middleware to dispatch 404 page --- bin/cli | 0 src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 bin/cli diff --git a/bin/cli b/bin/cli old mode 100644 new mode 100755 diff --git a/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php b/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php index 44fa3bf8..9b3cd6c4 100644 --- a/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php +++ b/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php @@ -85,7 +85,7 @@ class GenerateShortcodeMiddleware implements MiddlewareInterface } catch (\Exception $e) { $response->getBody()->write($e); } finally { - return is_callable($out) ? $out($request, $response) : $response; + return $response; } } } From aba7d3185a594f9358df14440e356fc10f9e9d5d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 1 May 2016 11:21:54 +0200 Subject: [PATCH 12/17] Added shortcode chars as an environment variable --- .env.dist | 1 + config/autoload/url-shortener.global.php | 9 ++++++--- .../CliRoutable/GenerateShortcodeMiddleware.php | 14 +++++++------- src/Service/UrlShortener.php | 4 ++-- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.env.dist b/.env.dist index 2e34016a..a25e8c2c 100644 --- a/.env.dist +++ b/.env.dist @@ -2,6 +2,7 @@ APP_ENV= SHORTENED_URL_SCHEMA= SHORTENED_URL_HOSTNAME= +SHORTCODE_CHARS= # Database DB_USER= diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index 795ce021..718ed3f0 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -1,9 +1,12 @@ [ - 'schema' => getenv('SHORTENED_URL_SCHEMA') ?: 'http', - 'hostname' => getenv('SHORTENED_URL_HOSTNAME'), + 'url_shortener' => [ + 'domain' => [ + 'schema' => getenv('SHORTENED_URL_SCHEMA') ?: 'http', + 'hostname' => getenv('SHORTENED_URL_HOSTNAME'), + ], + 'shortcode_chars' => getenv('SHORTCODE_CHARS'), ], ]; diff --git a/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php b/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php index 9b3cd6c4..6573ed60 100644 --- a/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php +++ b/src/Middleware/CliRoutable/GenerateShortcodeMiddleware.php @@ -19,20 +19,20 @@ class GenerateShortcodeMiddleware implements MiddlewareInterface /** * @var array */ - private $config; + private $domainConfig; /** * GenerateShortcodeMiddleware constructor. * * @param UrlShortenerInterface|UrlShortener $urlShortener - * @param array $config + * @param array $domainConfig * - * @Inject({UrlShortener::class, "config.url-shortener"}) + * @Inject({UrlShortener::class, "config.url_shortener.domain"}) */ - public function __construct(UrlShortenerInterface $urlShortener, array $config) + public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig) { $this->urlShortener = $urlShortener; - $this->config = $config; + $this->domainConfig = $domainConfig; } /** @@ -72,8 +72,8 @@ class GenerateShortcodeMiddleware implements MiddlewareInterface $shortcode = $this->urlShortener->urlToShortCode(new Uri($longUrl)); $shortUrl = (new Uri())->withPath($shortcode) - ->withScheme($this->config['schema']) - ->withHost($this->config['hostname']); + ->withScheme($this->domainConfig['schema']) + ->withHost($this->domainConfig['hostname']); $response->getBody()->write( sprintf('Processed URL "%s".%sGenerated short URL "%s"', $longUrl, PHP_EOL, $shortUrl) . PHP_EOL diff --git a/src/Service/UrlShortener.php b/src/Service/UrlShortener.php index 3fee0cf3..3c15a0f6 100644 --- a/src/Service/UrlShortener.php +++ b/src/Service/UrlShortener.php @@ -14,7 +14,7 @@ use Psr\Http\Message\UriInterface; class UrlShortener implements UrlShortenerInterface { - const DEFAULT_CHARS = 'rYHxLkXfsptbNZzKDG4hy85WFT7BRgMVdC9jvwQPnc6S32Jqm'; + const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ'; /** * @var ClientInterface @@ -35,7 +35,7 @@ class UrlShortener implements UrlShortenerInterface * @param EntityManagerInterface $em * @param string $chars * - * @Inject({"httpClient", "em"}) + * @Inject({"httpClient", "em", "config.url_shortener.shortcode_chars"}) */ public function __construct( ClientInterface $httpClient, From 75bcc4ec4c12acfac1da17c1a8e15e3df63f9147 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 1 May 2016 11:24:00 +0200 Subject: [PATCH 13/17] Fixed UrlShortenerTest after changing default chars --- tests/Service/UrlShortenerTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Service/UrlShortenerTest.php b/tests/Service/UrlShortenerTest.php index 010692a4..61580e0e 100644 --- a/tests/Service/UrlShortenerTest.php +++ b/tests/Service/UrlShortenerTest.php @@ -59,9 +59,9 @@ class UrlShortenerTest extends TestCase */ public function urlIsProperlyShortened() { - // 10 -> rY9zc + // 10 -> 12C1c $shortCode = $this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar')); - $this->assertEquals('rY9zc', $shortCode); + $this->assertEquals('12C1c', $shortCode); } /** @@ -112,16 +112,16 @@ class UrlShortenerTest extends TestCase */ public function shortCodeIsProperlyParsed() { - // rY9zc -> 10 + // 12C1c -> 10 $shortUrl = new ShortUrl(); - $shortUrl->setShortCode('rY9zc') + $shortUrl->setShortCode('12C1c') ->setOriginalUrl('expected_url'); $repo = $this->prophesize(ObjectRepository::class); - $repo->findOneBy(['shortCode' => 'rY9zc'])->willReturn($shortUrl); + $repo->findOneBy(['shortCode' => '12C1c'])->willReturn($shortUrl); $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); - $url = $this->urlShortener->shortCodeToUrl('rY9zc'); + $url = $this->urlShortener->shortCodeToUrl('12C1c'); $this->assertEquals($shortUrl->getOriginalUrl(), $url); } From bb1126914632aef439de7bff4bba5dc633409981 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 1 May 2016 13:36:23 +0200 Subject: [PATCH 14/17] Added more tests --- src/Middleware/CliParamsMiddleware.php | 9 +- .../Factory/CliParamsMiddlewareFactory.php | 5 +- tests/Middleware/CliParamsMiddlewareTest.php | 91 +++++++++++++++++++ .../CliParamsMiddlewareFactoryTest.php | 29 ++++++ 4 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 tests/Middleware/CliParamsMiddlewareTest.php create mode 100644 tests/Middleware/Factory/CliParamsMiddlewareFactoryTest.php diff --git a/src/Middleware/CliParamsMiddleware.php b/src/Middleware/CliParamsMiddleware.php index da243a15..683a8e86 100644 --- a/src/Middleware/CliParamsMiddleware.php +++ b/src/Middleware/CliParamsMiddleware.php @@ -12,10 +12,15 @@ class CliParamsMiddleware implements MiddlewareInterface * @var array */ private $argv; + /** + * @var + */ + private $currentSapi; - public function __construct(array $argv) + public function __construct(array $argv, $currentSapi) { $this->argv = $argv; + $this->currentSapi = $currentSapi; } /** @@ -46,7 +51,7 @@ class CliParamsMiddleware implements MiddlewareInterface public function __invoke(Request $request, Response $response, callable $out = null) { // When not in CLI, just call next middleware - if (php_sapi_name() !== 'cli') { + if ($this->currentSapi !== 'cli') { return $out($request, $response); } diff --git a/src/Middleware/Factory/CliParamsMiddlewareFactory.php b/src/Middleware/Factory/CliParamsMiddlewareFactory.php index b806cbeb..9be7f52e 100644 --- a/src/Middleware/Factory/CliParamsMiddlewareFactory.php +++ b/src/Middleware/Factory/CliParamsMiddlewareFactory.php @@ -24,6 +24,9 @@ class CliParamsMiddlewareFactory implements FactoryInterface */ public function __invoke(ContainerInterface $container, $requestedName, array $options = null) { - return new CliParamsMiddleware(isset($_SERVER['argv']) ? $_SERVER['argv'] : []); + return new CliParamsMiddleware( + isset($_SERVER['argv']) ? $_SERVER['argv'] : [], + php_sapi_name() + ); } } diff --git a/tests/Middleware/CliParamsMiddlewareTest.php b/tests/Middleware/CliParamsMiddlewareTest.php new file mode 100644 index 00000000..e07d05c0 --- /dev/null +++ b/tests/Middleware/CliParamsMiddlewareTest.php @@ -0,0 +1,91 @@ +__invoke( + ServerRequestFactory::fromGlobals(), + $originalResponse, + function ($req, $resp) use (&$invoked) { + $invoked = true; + return $resp; + } + ); + + $this->assertSame($originalResponse, $response); + $this->assertTrue($invoked); + } + + /** + * @test + */ + public function nonSuccessRouteResultJustInvokesNextMiddleware() + { + $middleware = new CliParamsMiddleware([], 'cli'); + + $invoked = false; + $originalResponse = new Response(); + $routeResult = $this->prophesize(RouteResult::class); + $routeResult->isSuccess()->willReturn(false)->shouldBeCalledTimes(1); + + $response = $middleware->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute(RouteResult::class, $routeResult->reveal()), + $originalResponse, + function ($req, $resp) use (&$invoked) { + $invoked = true; + return $resp; + } + ); + + $this->assertSame($originalResponse, $response); + $this->assertTrue($invoked); + } + + /** + * @test + */ + public function properRouteWillInjectAttributeInResponse() + { + $expectedLongUrl = 'http://www.google.com'; + $middleware = new CliParamsMiddleware(['foo', 'bar', $expectedLongUrl], 'cli'); + + $invoked = false; + $originalResponse = new Response(); + $routeResult = $this->prophesize(RouteResult::class); + $routeResult->isSuccess()->willReturn(true)->shouldBeCalledTimes(1); + $routeResult->getMatchedRouteName()->willReturn('cli-generate-shortcode')->shouldBeCalledTimes(1); + /** @var ServerRequestInterface $request */ + $request = null; + + $response = $middleware->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute(RouteResult::class, $routeResult->reveal()), + $originalResponse, + function ($req, $resp) use (&$invoked, &$request) { + $invoked = true; + $request = $req; + return $resp; + } + ); + + $this->assertSame($originalResponse, $response); + $this->assertEquals($expectedLongUrl, $request->getAttribute('longUrl')); + $this->assertTrue($invoked); + } +} diff --git a/tests/Middleware/Factory/CliParamsMiddlewareFactoryTest.php b/tests/Middleware/Factory/CliParamsMiddlewareFactoryTest.php new file mode 100644 index 00000000..3fad4bab --- /dev/null +++ b/tests/Middleware/Factory/CliParamsMiddlewareFactoryTest.php @@ -0,0 +1,29 @@ +factory = new CliParamsMiddlewareFactory(); + } + + /** + * @test + */ + public function serviceIsCreated() + { + $instance = $this->factory->__invoke(new ServiceManager(), ''); + $this->assertInstanceOf(CliParamsMiddleware::class, $instance); + } +} From 83352f0af9f88c885a271a022e69070b3e9c33c9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 1 May 2016 17:54:56 +0200 Subject: [PATCH 15/17] Added more tests --- src/Service/VisitsTracker.php | 2 +- tests/Factory/CacheFactoryTest.php | 46 +++++++++++++++++++++++++++++ tests/Service/VisitsTrackerTest.php | 30 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 tests/Factory/CacheFactoryTest.php create mode 100644 tests/Service/VisitsTrackerTest.php diff --git a/src/Service/VisitsTracker.php b/src/Service/VisitsTracker.php index 80598053..453627d9 100644 --- a/src/Service/VisitsTracker.php +++ b/src/Service/VisitsTracker.php @@ -42,7 +42,7 @@ class VisitsTracker implements VisitsTrackerInterface $visit = new Visit(); $visit->setShortUrl($shortUrl) ->setUserAgent($this->getArrayValue($visitorData, 'HTTP_USER_AGENT')) - ->setReferer($this->getArrayValue($visitorData, 'REFERER')) + ->setReferer($this->getArrayValue($visitorData, 'HTTP_REFERER')) ->setRemoteAddr($this->getArrayValue($visitorData, 'REMOTE_ADDR')); $this->em->persist($visit); $this->em->flush(); diff --git a/tests/Factory/CacheFactoryTest.php b/tests/Factory/CacheFactoryTest.php new file mode 100644 index 00000000..a6920d56 --- /dev/null +++ b/tests/Factory/CacheFactoryTest.php @@ -0,0 +1,46 @@ +factory = new CacheFactory(); + } + + public static function tearDownAfterClass() + { + putenv('APP_ENV'); + } + + /** + * @test + */ + public function productionReturnsApcAdapter() + { + putenv('APP_ENV=pro'); + $instance = $this->factory->__invoke(new ServiceManager(), ''); + $this->assertInstanceOf(ApcuCache::class, $instance); + } + + /** + * @test + */ + public function developmentReturnsArrayAdapter() + { + putenv('APP_ENV=dev'); + $instance = $this->factory->__invoke(new ServiceManager(), ''); + $this->assertInstanceOf(ArrayCache::class, $instance); + } +} diff --git a/tests/Service/VisitsTrackerTest.php b/tests/Service/VisitsTrackerTest.php new file mode 100644 index 00000000..7e6707c7 --- /dev/null +++ b/tests/Service/VisitsTrackerTest.php @@ -0,0 +1,30 @@ +prophesize(EntityRepository::class); + $repo->findOneBy(['shortCode' => $shortCode])->willReturn(new ShortUrl()); + + $em = $this->prophesize(EntityManager::class); + $em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1); + $em->persist(Argument::any())->shouldBeCalledTimes(1); + $em->flush()->shouldBeCalledTimes(1); + + $visitsTracker = new VisitsTracker($em->reveal()); + $visitsTracker->track($shortCode); + } +} From 3e9161efb0c956ab68ef460d6b7d69af3d67fc2f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 2 May 2016 17:17:11 +0200 Subject: [PATCH 16/17] Updated templates --- templates/app/home-page.html.twig | 113 ----------------------------- templates/error/404.html.twig | 17 +++-- templates/error/error.html.twig | 15 ++-- templates/layout/default.html.twig | 11 ++- 4 files changed, 26 insertions(+), 130 deletions(-) delete mode 100644 templates/app/home-page.html.twig diff --git a/templates/app/home-page.html.twig b/templates/app/home-page.html.twig deleted file mode 100644 index b048b139..00000000 --- a/templates/app/home-page.html.twig +++ /dev/null @@ -1,113 +0,0 @@ -{% extends 'layout/default.html.twig' %} - -{% block title %}Home{% endblock %} - -{% block content %} -
-

Welcome to zend-expressive

-

- Congratulations! You have successfully installed the - zend-expressive skeleton application. - This skeleton can serve as a simple starting point for you to begin building your application. -

-

- Expressive builds on zend-stratigility to provide a minimalist PSR-7 middleware framework for PHP. -

-
- -
-
-

- - Agile & Lean - -

-

- Expressive is fast, small and perfect for rapid application development, prototyping and api's. You decide how you - extend it and choose the best packages from major framework or standalone projects. -

-
- -
-

- - HTTP Messages - -

-

- HTTP messages are the foundation of web development. Web browsers and HTTP clients such as cURL create - HTTP request messages that are sent to a web server, which provides an HTTP response message. - Server-side code receives an HTTP request message, and returns an HTTP response message. -

-
- -
-

- - Middleware - -

-

- Middleware is code that exists between the request and response, and which can take the incoming - request, perform actions based on it, and either complete the response or pass delegation on to the - next middleware in the queue. Your application is easily extended with custom middleware created by - yourself or others. -

-
-
- -
-
-

- - Containers - -

-

- Expressive promotes and advocates the usage of Dependency Injection/Inversion of Control containers - when writing your applications. Expressive supports multiple containers which typehints against - container-interop. -

-
- -
-

- - Routers - -

-

- One fundamental feature of zend-expressive is that it provides mechanisms for implementing dynamic - routing, a feature required in most modern web applications. Expressive ships with multiple adapters. -

- {% if routerName is defined %} -

- - Get started with {{ routerName }}. - -

- {% endif %} -
- -
-

- - Templating - -

-

- By default, no middleware in Expressive is templated. We do not even provide a default templating - engine, as the choice of templating engine is often very specific to the project and/or organization. - However, Expressive does provide abstraction for templating, which allows you to write middleware that - is engine-agnostic. -

- {% if templateName is defined %} -

- - Get started with {{ templateName }}. - -

- {% endif %} -
-
-{% endblock %} diff --git a/templates/error/404.html.twig b/templates/error/404.html.twig index 8e3f3a7a..0e591e2a 100644 --- a/templates/error/404.html.twig +++ b/templates/error/404.html.twig @@ -1,12 +1,17 @@ {% extends 'layout/default.html.twig' %} -{% block title %}404 Not Found{% endblock %} +{% block title %}URL Not Found{% endblock %} + +{% block stylesheets %} + +{% endblock %} {% block content %}

Oops!

-

This is awkward.

-

We encountered a 404 Not Found error.

-

- You are looking for something that doesn't exist or may have moved. -

+
+

This short URL doesn't seem to be valid.

+

Make sure you included all the characters, with no extra punctuation.

{% endblock %} diff --git a/templates/error/error.html.twig b/templates/error/error.html.twig index c9b66893..cd54354e 100644 --- a/templates/error/error.html.twig +++ b/templates/error/error.html.twig @@ -2,14 +2,19 @@ {% block title %}{{ status }} {{ reason }}{% endblock %} +{% block stylesheets %} + +{% endblock %} + {% block content %}

Oops!

-

This is awkward.

+

We encountered a {{ status }} {{ reason }} error.

{% if status == 404 %} -

- You are looking for something that doesn't exist or may have moved. Check out one of the links on this page - or head back to Home. -

+

This short URL doesn't seem to be valid.

+

Make sure you included all the characters, with no extra punctuation.

{% endif %} {% endblock %} diff --git a/templates/layout/default.html.twig b/templates/layout/default.html.twig index ac34990a..7c29df7a 100644 --- a/templates/layout/default.html.twig +++ b/templates/layout/default.html.twig @@ -3,16 +3,15 @@ - {% block title %}{% endblock %} - zend-expressive + {% block title %}{% endblock %} | URL shortener {% block stylesheets %}{% endblock %} From 4e852aab9b6f955e70556e1c309cc88344e1408f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 2 May 2016 17:20:07 +0200 Subject: [PATCH 17/17] Fixed typo and added link to my website --- src/Middleware/Routable/RedirectMiddleware.php | 2 +- templates/layout/default.html.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Middleware/Routable/RedirectMiddleware.php b/src/Middleware/Routable/RedirectMiddleware.php index 104acd44..85b42eb6 100644 --- a/src/Middleware/Routable/RedirectMiddleware.php +++ b/src/Middleware/Routable/RedirectMiddleware.php @@ -77,7 +77,7 @@ class RedirectMiddleware implements MiddlewareInterface $this->visitTracker->track($shortCode); // Return a redirect response to the long URL. - // Use a temporary redirect to make sure browsers aleways hit the server for analytics purposes + // Use a temporary redirect to make sure browsers always hit the server for analytics purposes return new RedirectResponse($longUrl); } catch (\Exception $e) { // In case of error, dispatch 404 error diff --git a/templates/layout/default.html.twig b/templates/layout/default.html.twig index 7c29df7a..aaec0aab 100644 --- a/templates/layout/default.html.twig +++ b/templates/layout/default.html.twig @@ -27,7 +27,7 @@
{% block footer %}

- © {{ "now" | date("Y") }} by Alejandro Celaya. + © {{ "now" | date("Y") }} by Alejandro Celaya.

{% endblock %}