From ff69da66b6909ded76e0bb496babc696960b947e Mon Sep 17 00:00:00 2001 From: Nathachai Thongniran Date: Fri, 31 Mar 2017 01:46:38 +0700 Subject: [PATCH] First release --- .gitignore | 11 ++ .travis.yml | 25 +++ Controller/EncodeController.php | 42 +++++ Controller/RedirectController.php | 27 ++++ DependencyInjection/Configuration.php | 39 +++++ .../LeoparddUrlShortenerExtension.php | 43 +++++ Entity/ShortUrl.php | 125 +++++++++++++++ Entity/ShortUrlInterface.php | 37 +++++ Event/ShortUrlCreatedEvent.php | 33 ++++ Event/ShortUrlEvent.php | 13 ++ Event/ShortUrlRedirectedEvent.php | 33 ++++ Exception/InvalidCodeException.php | 17 ++ Exception/InvalidUrlException.php | 17 ++ Factory/ShortUrlFactory.php | 34 ++++ Factory/ShortUrlFactoryInterface.php | 19 +++ LICENSE | 21 --- LICENSE.md | 21 +++ LeoparddUrlShortenerBundle.php | 9 ++ README.md | 151 +++++++++++++++++- Repository/ShortUrlRepository.php | 48 ++++++ Repository/ShortUrlRepositoryInterface.php | 30 ++++ Resources/config/routing.yml | 11 ++ Resources/config/services.yml | 40 +++++ Service/EncodeService.php | 62 +++++++ Service/RedirectService.php | 60 +++++++ composer.json | 50 ++++++ phpspec.yml | 4 + ruleset.xml | 7 + spec/Service/EncodeServiceSpec.php | 84 ++++++++++ spec/Service/RedirectServiceSpec.php | 93 +++++++++++ 30 files changed, 1184 insertions(+), 22 deletions(-) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Controller/EncodeController.php create mode 100644 Controller/RedirectController.php create mode 100644 DependencyInjection/Configuration.php create mode 100644 DependencyInjection/LeoparddUrlShortenerExtension.php create mode 100644 Entity/ShortUrl.php create mode 100644 Entity/ShortUrlInterface.php create mode 100644 Event/ShortUrlCreatedEvent.php create mode 100644 Event/ShortUrlEvent.php create mode 100644 Event/ShortUrlRedirectedEvent.php create mode 100644 Exception/InvalidCodeException.php create mode 100644 Exception/InvalidUrlException.php create mode 100644 Factory/ShortUrlFactory.php create mode 100644 Factory/ShortUrlFactoryInterface.php delete mode 100644 LICENSE create mode 100644 LICENSE.md create mode 100644 LeoparddUrlShortenerBundle.php create mode 100644 Repository/ShortUrlRepository.php create mode 100644 Repository/ShortUrlRepositoryInterface.php create mode 100644 Resources/config/routing.yml create mode 100644 Resources/config/services.yml create mode 100644 Service/EncodeService.php create mode 100644 Service/RedirectService.php create mode 100644 composer.json create mode 100644 phpspec.yml create mode 100644 ruleset.xml create mode 100644 spec/Service/EncodeServiceSpec.php create mode 100644 spec/Service/RedirectServiceSpec.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c563bc2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# IDE +.idea + +# Binary +bin + +# Dependency +vendor + +# Lock +composer.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b9eb92b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +sudo: false +language: php +php: + - 5.6 + - 7.0 + - hhvm +matrix: + allow_failures: + - php: nightly +cache: + directories: + - $HOME/.composer/cache + - vendor +branches: + only: + - master +notifications: + email: false +before_script: + - travis_retry composer self-update + - travis_retry composer install --no-interaction --prefer-source --dev +script: + - php --version + - composer --version + - composer check diff --git a/Controller/EncodeController.php b/Controller/EncodeController.php new file mode 100644 index 0000000..bf7e48c --- /dev/null +++ b/Controller/EncodeController.php @@ -0,0 +1,42 @@ +get('leopardd_url_shortener.factory.short_url'); + + /** @var EncodeService $encodeService */ + $encodeService = $this->get('leopardd_url_shortener.service.encode'); + + $url = $request->request->get('url'); + + // validate and sanitize + if (filter_var($url, FILTER_VALIDATE_URL) === false) throw new InvalidUrlException(); + $url = rtrim($url, '/'); + + $shortUrl = $shortUrlFactory->create($url); + $shortUrl = $encodeService->process($shortUrl); + + return new JsonResponse([ + 'url' => $shortUrl->getUrl(), + 'code' => $shortUrl->getCode() + ]); + } +} diff --git a/Controller/RedirectController.php b/Controller/RedirectController.php new file mode 100644 index 0000000..22e861a --- /dev/null +++ b/Controller/RedirectController.php @@ -0,0 +1,27 @@ +get('leopardd_url_shortener.service.redirect'); + + $response = $redirectService->getRedirectResponse($code); + if ($response === null) throw new InvalidCodeException(); + + return $response; + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php new file mode 100644 index 0000000..80ec0a9 --- /dev/null +++ b/DependencyInjection/Configuration.php @@ -0,0 +1,39 @@ +root('leopardd_url_shortener'); + + // Here you should define the parameters that are allowed to + // configure your bundle. See the documentation linked above for + // more information on that topic. + $rootNode + ->children() + ->arrayNode('hashids') + ->children() + ->scalarNode('salt')->end() + ->integerNode('min_length')->end() + ->scalarNode('alphabet')->end() + ->end() + ->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/DependencyInjection/LeoparddUrlShortenerExtension.php b/DependencyInjection/LeoparddUrlShortenerExtension.php new file mode 100644 index 0000000..5bdb9db --- /dev/null +++ b/DependencyInjection/LeoparddUrlShortenerExtension.php @@ -0,0 +1,43 @@ +processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('services.yml'); + + // Hashids Config + if (isset($config['hashids'])) { + if (isset($config['hashids']['salt'])) { + $container->setParameter('leopardd_url_shortener.hashids.salt', $config['hashids']['salt']); + } + + if (isset($config['hashids']['min_length'])) { + $container->setParameter('leopardd_url_shortener.hashids.min_length', $config['hashids']['min_length']); + } + + if (isset($config['hashids']['alphabet'])) { + $container->setParameter('leopardd_url_shortener.hashids.alphabet', $config['hashids']['alphabet']); + } + } + } +} diff --git a/Entity/ShortUrl.php b/Entity/ShortUrl.php new file mode 100644 index 0000000..32e0d58 --- /dev/null +++ b/Entity/ShortUrl.php @@ -0,0 +1,125 @@ +") + * @JMS\Expose() + */ + private $created; + + /** + * ShortUrl constructor. + */ + public function __construct() + { + $this->created = new DateTime(); + } + + /** + * {@inheritdoc} + */ + public function getId() + { + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function getCode() + { + return $this->code; + } + + /** + * {@inheritdoc} + */ + public function setCode($code) + { + $this->code = $code; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getUrl() + { + return $this->url; + } + + /** + * {@inheritdoc} + */ + public function setUrl($url) + { + $this->url = $url; + + return $this; + } + + /** + * @return DateTime + */ + public function getCreated() + { + return $this->created; + } + + /** + * @param DateTime $created + * @return ShortUrl + */ + public function setCreated($created) + { + $this->created = $created; + + return $this; + } +} diff --git a/Entity/ShortUrlInterface.php b/Entity/ShortUrlInterface.php new file mode 100644 index 0000000..2372ae6 --- /dev/null +++ b/Entity/ShortUrlInterface.php @@ -0,0 +1,37 @@ +shortUrl = $shortUrl; + } + + /** + * @return ShortUrlInterface + */ + public function getShortUrl() + { + return $this->shortUrl; + } +} diff --git a/Event/ShortUrlEvent.php b/Event/ShortUrlEvent.php new file mode 100644 index 0000000..f1547f3 --- /dev/null +++ b/Event/ShortUrlEvent.php @@ -0,0 +1,13 @@ +shortUrl = $shortUrl; + } + + /** + * @return ShortUrlInterface + */ + public function getShortUrl() + { + return $this->shortUrl; + } +} diff --git a/Exception/InvalidCodeException.php b/Exception/InvalidCodeException.php new file mode 100644 index 0000000..4945ace --- /dev/null +++ b/Exception/InvalidCodeException.php @@ -0,0 +1,17 @@ +shortUrl = new $shortUrl(); + } + + /** + * {@inheritdoc} + */ + public function create($url) + { + $this->shortUrl->setUrl($url); + + return $this->shortUrl; + } +} diff --git a/Factory/ShortUrlFactoryInterface.php b/Factory/ShortUrlFactoryInterface.php new file mode 100644 index 0000000..6e89782 --- /dev/null +++ b/Factory/ShortUrlFactoryInterface.php @@ -0,0 +1,19 @@ + + +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. diff --git a/LeoparddUrlShortenerBundle.php b/LeoparddUrlShortenerBundle.php new file mode 100644 index 0000000..ffe83d4 --- /dev/null +++ b/LeoparddUrlShortenerBundle.php @@ -0,0 +1,9 @@ +createQueryBuilder('s'); + $qb->where('s.id = :id') + ->setParameter('id', $id); + + return $qb->getQuery()->getOneOrNullResult(); + } + + /** + * {@inheritdoc} + */ + public function findOneByUrl($url) + { + $qb = $this->createQueryBuilder('s'); + $qb->where('s.url = :url') + ->setParameter('url', $url); + + return $qb->getQuery()->getOneOrNullResult(); + } + + /** + * {@inheritdoc} + */ + public function save(ShortUrlInterface $shortUrl) + { + $this->_em->persist($shortUrl); + $this->_em->flush($shortUrl); + + return $shortUrl; + } +} diff --git a/Repository/ShortUrlRepositoryInterface.php b/Repository/ShortUrlRepositoryInterface.php new file mode 100644 index 0000000..0b34109 --- /dev/null +++ b/Repository/ShortUrlRepositoryInterface.php @@ -0,0 +1,30 @@ +hashids = $hashids; + $this->shortUrlRepository = $shortUrlRepository; + $this->dispatcher = $dispatcher; + } + + /** + * @param ShortUrlInterface $shortUrl + * @return ShortUrlInterface + */ + public function process($shortUrl) + { + $existentShortUrl = $this->shortUrlRepository->findOneByUrl($shortUrl->getUrl()); + if ($existentShortUrl) return $existentShortUrl; + + $this->shortUrlRepository->save($shortUrl); + + $shortUrlId = $shortUrl->getId(); + $code = $this->hashids->encode($shortUrlId); + + $shortUrl->setCode($code); + $shortUrl = $this->shortUrlRepository->save($shortUrl); + + $event = new ShortUrlCreatedEvent($shortUrl); + $this->dispatcher->dispatch(ShortUrlEvent::SHORT_URL_CREATED, $event); + + return $shortUrl; + } +} diff --git a/Service/RedirectService.php b/Service/RedirectService.php new file mode 100644 index 0000000..6cde606 --- /dev/null +++ b/Service/RedirectService.php @@ -0,0 +1,60 @@ +hashids = $hashids; + $this->shortUrlRepository = $shortUrlRepository; + $this->dispatcher = $dispatcher; + } + + /** + * @param string $code + * @throws InvalidCodeException + * @return RedirectResponse|null + */ + public function getRedirectResponse($code) + { + $ids = $this->hashids->decode($code); + if (!isset($ids[0]) || !is_numeric($ids[0])) throw new InvalidCodeException(); + $id = $ids[0]; + + $shortUrl = $this->shortUrlRepository->findOneById($id); + if (!$shortUrl) return null; + + $event = new ShortUrlRedirectedEvent($shortUrl); + $this->dispatcher->dispatch(ShortUrlEvent::SHORT_URL_REDIRECTED, $event); + + return new RedirectResponse($shortUrl->getUrl()); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e815d10 --- /dev/null +++ b/composer.json @@ -0,0 +1,50 @@ +{ + "name": "leopardd/url-shortener-bundle", + "description": "Symfony Bundle to generate and redirect short url", + "type": "symfony-bundle", + "license": "MIT", + "keywords": [ + "url", + "shortener", + "link", + "bitly", + "googl", + "symfony", + "symfony-bundle" + ], + "authors": [ + { + "name": "Nathachai Thongniran", + "email": "inid3a@gmail.com", + "homepage": "https://github.com/jojoee", + "role": "Developer" + } + ], + "require": { + "php": "~5.6|~7.0", + "symfony/framework-bundle": "~2.6|~3.0", + "doctrine/orm": "^2.5", + "doctrine/doctrine-bundle": "^1.6", + "hashids/hashids": "^2.0", + "jms/serializer-bundle": "^1.3" + }, + "require-dev": { + "phpspec/phpspec": "^3.1", + "sensio/generator-bundle": "^3.0", + "squizlabs/php_codesniffer": "^2.8", + "symfony/phpunit-bridge": "^3.0" + }, + "autoload": { + "psr-4": { + "Leopardd\\Bundle\\UrlShortenerBundle\\": "" + } + }, + "scripts": { + "phpspec": "phpspec run -c phpspec.yml --stop-on-failure -v", + "phpcs": "phpcs -p -v --standard=ruleset.xml --extensions=php --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 --ignore=*/vendor/*,*/bin/*,*/spec/* ./", + "check": "composer phpspec && composer phpcs" + }, + "config": { + "bin-dir": "bin" + } +} diff --git a/phpspec.yml b/phpspec.yml new file mode 100644 index 0000000..62d08f0 --- /dev/null +++ b/phpspec.yml @@ -0,0 +1,4 @@ +suites: + acme_suite: + namespace: Leopardd\Bundle\UrlShortenerBundle + psr4_prefix: Leopardd\Bundle\UrlShortenerBundle diff --git a/ruleset.xml b/ruleset.xml new file mode 100644 index 0000000..473ccfa --- /dev/null +++ b/ruleset.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spec/Service/EncodeServiceSpec.php b/spec/Service/EncodeServiceSpec.php new file mode 100644 index 0000000..93a74e6 --- /dev/null +++ b/spec/Service/EncodeServiceSpec.php @@ -0,0 +1,84 @@ +beConstructedWith($hashids, $shortUrlRepository, $dispatcher); + } + + function it_is_initializable() + { + $this->shouldHaveType(EncodeService::class); + } + + function it_process_existent_url( + ShortUrlInterface $shortUrl, + ShortUrlInterface $existentShortUrl, + ShortUrlRepositoryInterface $shortUrlRepository + ) { + $url = 'https://www.google.com'; + + $shortUrl->getUrl() + ->shouldBeCalled() + ->willReturn($url); + $shortUrlRepository->findOneByUrl($url) + ->shouldBeCalled() + ->willReturn($existentShortUrl); + + $this->process($shortUrl)->shouldReturn($existentShortUrl); + } + + function it_process( + EventDispatcherInterface $dispatcher, + Hashids $hashids, + ShortUrlInterface $shortUrl, + ShortUrlRepositoryInterface $shortUrlRepository + ) { + $id = 8; + $url = 'https://www.google.com'; + $code = 'abc'; + + $shortUrl->getUrl() + ->shouldBeCalled() + ->willReturn($url); + $shortUrlRepository->findOneByUrl($url) + ->shouldBeCalled() + ->willReturn(null); + $shortUrlRepository->save($shortUrl) + ->shouldBeCalledTimes(2) + ->willReturn($shortUrl); + $shortUrl->getId() + ->shouldBeCalled() + ->willReturn($id); + $hashids->encode($id) + ->willReturn($code); + $shortUrl->setCode($code) + ->shouldBeCalled(); + + $event = new ShortUrlCreatedEvent($shortUrl->getWrappedObject()); + $dispatcher->dispatch(ShortUrlEvent::SHORT_URL_CREATED, $event) + ->shouldBeCalled(); + + $this->process($shortUrl)->shouldReturn($shortUrl); + } +} diff --git a/spec/Service/RedirectServiceSpec.php b/spec/Service/RedirectServiceSpec.php new file mode 100644 index 0000000..8a91a72 --- /dev/null +++ b/spec/Service/RedirectServiceSpec.php @@ -0,0 +1,93 @@ +beConstructedWith($hashids, $shortUrlRepository, $dispatcher); + } + + function it_is_initializable() + { + $this->shouldHaveType(RedirectService::class); + } + + function it_get_redirect_response_by_invalid_code( + Hashids $hashids + ) { + $id = 'not numeric'; + $code = 'abc'; + + $hashids->decode($code) + ->shouldBeCalled() + ->willReturn($id); + + $this->shouldThrow(InvalidCodeException::class)->duringGetRedirectResponse($code); + } + + function it_get_redirect_response_by_nonexistent_code( + Hashids $hashids, + ShortUrlRepositoryInterface $shortUrlRepository + ) { + $ids = [8]; + $id = $ids[0]; + $code = 'abc'; + + $hashids->decode($code) + ->shouldBeCalled() + ->willReturn($ids); + $shortUrlRepository->findOneById($id) + ->shouldBeCalled() + ->willReturn(null); + + $this->getRedirectResponse($code)->shouldReturn(null); + } + + function it_get_redirect_response( + EventDispatcherInterface $dispatcher, + Hashids $hashids, + ShortUrlInterface $shortUrl, + ShortUrlRepositoryInterface $shortUrlRepository + ) { + $ids = [8]; + $id = $ids[0]; + $url = 'https://www.google.com'; + $code = 'abc'; + $redirectResponse = new RedirectResponse($url); + + $hashids->decode($code) + ->shouldBeCalled() + ->willReturn($ids); + $shortUrlRepository->findOneById($id) + ->shouldBeCalled() + ->willReturn($shortUrl); + $dispatcher->dispatch(ShortUrlEvent::SHORT_URL_REDIRECTED, Argument::type(ShortUrlRedirectedEvent::class)) + ->shouldBeCalled(); + $shortUrl->getUrl() + ->shouldBeCalled() + ->willReturn($url); + + $this->getRedirectResponse($code)->shouldBeLike($redirectResponse); + } +}