Compare commits

...

94 Commits

Author SHA1 Message Date
Alejandro Celaya
9ab4b9ab43 Merge branch 'develop' 2016-08-01 21:30:49 +02:00
Alejandro Celaya
5e493b435a Added badges to readme file 2016-08-01 21:26:47 +02:00
Alejandro Celaya
21dc9ac5a0 Updated changelog for version 1 2016-08-01 21:25:15 +02:00
Alejandro Celaya
40a3ba6203 Improved rest documentation 2016-08-01 21:15:30 +02:00
Alejandro Celaya
307dfc64b4 Created installation steps doc 2016-08-01 21:13:17 +02:00
Alejandro Celaya
9b9b1415fe Created GenerateCharsetCommand 2016-08-01 21:11:42 +02:00
Alejandro Celaya
ce2b28a0b4 Improved cli entry point so that the language is set to the translator based on the CLI_LANGUAGE env var 2016-08-01 20:44:45 +02:00
Alejandro Celaya
7b98527f2e Improved CacheFactory so that adapter can be set in config 2016-08-01 20:16:13 +02:00
Alejandro Celaya
30988b10d1 Added Laravel's env helper 2016-08-01 14:36:43 +02:00
Alejandro Celaya
d73d3049b7 Removed dependency on expressive-helpers package 2016-07-31 16:42:19 +02:00
Alejandro Celaya
7b1e855e7f Renamed services config files to dependencies 2016-07-31 16:32:27 +02:00
Alejandro Celaya
a957f66ed0 Renamed services first level config key to dependencies 2016-07-31 16:30:05 +02:00
Alejandro Celaya
ce00874dd1 Merge branch 'feature/12' into develop 2016-07-31 16:25:11 +02:00
Alejandro Celaya
3d5e5d5df9 Created ResolveUrlActionTest 2016-07-31 16:24:00 +02:00
Alejandro Celaya
08f6d2de78 Created ListShortcodesActionTest 2016-07-31 16:16:26 +02:00
Alejandro Celaya
c6b7515285 Created GetVisitsActionTest 2016-07-31 16:10:16 +02:00
Alejandro Celaya
04e0a192ad Created CreateShortcodeActionTest 2016-07-31 15:58:18 +02:00
Alejandro Celaya
878518ced7 Created AuthenticateActionTest 2016-07-31 13:33:55 +02:00
Alejandro Celaya
6f7e4f7e7f Created RestUtilsTest 2016-07-31 13:18:36 +02:00
Alejandro Celaya
f701e65f75 Created RestTokenServiceTest 2016-07-31 13:14:06 +02:00
Alejandro Celaya
ef6f4fba66 Improved CheckAuthenticationMiddlewareTest 2016-07-31 13:01:08 +02:00
Alejandro Celaya
f904f79c18 Created CheckAuthenticationMiddlewareTest 2016-07-30 23:26:49 +02:00
Alejandro Celaya
41939b790d Added more Rest module tests 2016-07-30 23:17:13 +02:00
Alejandro Celaya
8c446f0f3b Created Core\ConfigProviderTest 2016-07-30 23:07:44 +02:00
Alejandro Celaya
9c6420fe26 Created VisitServiceTest 2016-07-30 23:01:07 +02:00
Alejandro Celaya
ce4877d4ac Improved VisitsTrackerTest 2016-07-30 22:55:28 +02:00
Alejandro Celaya
ebeaa3c64a Created RedirectActionTest 2016-07-30 20:02:48 +02:00
Alejandro Celaya
fcdcfde04f Added missing tests for Common module 2016-07-30 17:45:48 +02:00
Alejandro Celaya
2ce6c1f44b Added more tests to Common module 2016-07-30 17:17:21 +02:00
Alejandro Celaya
00db8a7ea5 Created Common\ErrorHandler tests 2016-07-30 17:07:35 +02:00
Alejandro Celaya
4c6cc9cd11 Created ResolveUrlCommandTest 2016-07-30 16:48:02 +02:00
Alejandro Celaya
50f1549457 Created ProcessVisitsCommand 2016-07-30 14:42:09 +02:00
Alejandro Celaya
3923bf0604 Created GetVisitsCommandTest 2016-07-30 14:30:30 +02:00
Alejandro Celaya
bb8404040a Created GenerateShortcodeCommandTest 2016-07-30 14:13:28 +02:00
Alejandro Celaya
5cdd782ce1 Removed whiteline 2016-07-30 13:54:00 +02:00
Alejandro Celaya
e345c2bbfe Moved error handler classes from Expressive namespace to ErrorHandler namespace 2016-07-30 13:51:52 +02:00
Alejandro Celaya
2a018f5415 Fixed ContentBasedErrorHandler fatching error handlers from the composed plugin manager 2016-07-30 10:47:29 +02:00
Alejandro Celaya
7394424a43 Merge branch 'develop' into feature/12 2016-07-30 10:41:05 +02:00
Alejandro Celaya
2f5119d0b3 Split ContentBasedErrorHandler responsibilities into two separated classes 2016-07-30 10:08:34 +02:00
Alejandro Celaya
c569cef239 Fixed ContentBased error handler not using the default content if accepted contents are not valid 2016-07-29 11:25:53 +02:00
Alejandro Celaya
ab6aa99a6d Created more tests on CLI module 2016-07-28 20:49:27 +02:00
Alejandro Celaya
f4532c3015 Replaced exclusive ifs by if-else 2016-07-28 09:56:44 +02:00
Alejandro Celaya
af9193f721 Removed duplicated error handling for 404 errors 2016-07-28 09:40:36 +02:00
Alejandro Celaya
36259588db Fixed Action prefix on routable middlewares 2016-07-27 20:22:50 +02:00
Alejandro Celaya
a3554db1c3 Merge branch 'feature/15' into develop 2016-07-27 20:18:28 +02:00
Alejandro Celaya
75e744838c Created content based error handler which allows managing errors in a different way depending on the Accepted content type from the client 2016-07-27 20:17:23 +02:00
Alejandro Celaya
f3d2cf5e15 Deleted ResponseTypeMiddleware which is not ussable anymore 2016-07-26 19:10:43 +02:00
Alejandro Celaya
a81dba58bd Defined custom NotFoundMiddleware for rest routes 2016-07-26 19:10:01 +02:00
Alejandro Celaya
83f29080c6 Improved the way rest errors are catched 2016-07-26 11:05:17 +02:00
Alejandro Celaya
0ef1e416c6 Created middleware to catch rest errors and return JSON responses 2016-07-26 09:54:13 +02:00
Alejandro Celaya
57d81115de Added PHP 7.1 to the CI matrix 2016-07-21 18:46:52 +02:00
Alejandro Celaya
5c8353da02 Set collation to utf8_bin in shortCode column of ShortUrl so that the UNIQUE key is case sensitive 2016-07-21 18:46:15 +02:00
Alejandro Celaya
95346ebb7e Merge branch 'feature/23' into develop 2016-07-21 17:00:42 +02:00
Alejandro Celaya
cd5bbcd60a Reused middleware to check Accept-Language header on any HTTP related middleware 2016-07-21 16:59:27 +02:00
Alejandro Celaya
fb9c7f8eec Used twig extension to load translations on twig templates 2016-07-21 16:54:00 +02:00
Alejandro Celaya
e42469b090 Added translations for error messages returned by the REST API 2016-07-21 16:41:16 +02:00
Alejandro Celaya
06868f782b Created middleware for rest that reads the language from the Accept-Language header 2016-07-21 16:20:27 +02:00
Alejandro Celaya
73a35a8f44 Added translator and translations to ResolveUrlCommand 2016-07-21 16:01:16 +02:00
Alejandro Celaya
6a05265a48 Added translator and translations to ProcessVisitsCommand 2016-07-21 15:50:27 +02:00
Alejandro Celaya
8e51b51cae Added translator and translations to ListShortcodesCommand 2016-07-21 15:43:19 +02:00
Alejandro Celaya
545fe7da70 Added translator and translations to GetVisitsCommand 2016-07-21 15:35:52 +02:00
Alejandro Celaya
cb99130c1e Created translator and used inside one of the commands 2016-07-21 15:08:46 +02:00
Alejandro Celaya
b413b16c86 Merge branch 'feature/21' into develop 2016-07-21 10:16:52 +02:00
Alejandro Celaya
3ba51c5390 Improved visits REST endpoint path 2016-07-21 10:16:36 +02:00
Alejandro Celaya
0ef9db0bdf Added option to filter by date in visits REST endpoint 2016-07-21 10:13:09 +02:00
Alejandro Celaya
bdd2d6f8b2 Improved DateRange to check if both wrapped dates are empty 2016-07-21 10:03:37 +02:00
Alejandro Celaya
45d194aced Added option to filter by date in shortcode:views CLI command 2016-07-21 09:58:33 +02:00
Alejandro Celaya
0a57f52309 Fixed typo 2016-07-21 09:46:12 +02:00
Alejandro Celaya
d97287ab0c Added option to filter by date the visits list 2016-07-21 09:36:38 +02:00
Alejandro Celaya
84c4021b24 Merge branch 'feature/22' into develop 2016-07-20 19:04:48 +02:00
Alejandro Celaya
d39a949992 Fixed wrong exception name 2016-07-20 19:04:38 +02:00
Alejandro Celaya
dbe1281d2a Created services and command to process visits 2016-07-20 19:00:23 +02:00
Alejandro Celaya
d3c2f4ed2a Created service to resolve IP locations 2016-07-20 12:37:48 +02:00
Alejandro Celaya
06fa33877b Moved some exceptions from core to common 2016-07-20 10:13:53 +02:00
Alejandro Celaya
c290bed354 Created VisitLocation entity 2016-07-20 09:35:46 +02:00
Alejandro Celaya
31af0eea04 Improved main config file and fixed tests whitelist 2016-07-19 23:35:47 +02:00
Alejandro Celaya
aaf4f1dfe5 Improved config loading so that autoloaded overrides module-specific 2016-07-19 23:30:23 +02:00
Alejandro Celaya
e28e984278 Improved CrossDomainMiddleware by allowing the same origin that was requested 2016-07-19 22:38:14 +02:00
Alejandro Celaya
839329d627 Improved CrossDomainMiddleware preventing headers to be injected on non-CORS requests 2016-07-19 20:20:18 +02:00
Alejandro Celaya
0daa24739d Merge branch 'feature/11' into develop 2016-07-19 18:34:02 +02:00
Alejandro Celaya
7ca52ecff9 Removed wrong use statement from old namespace 2016-07-19 18:33:45 +02:00
Alejandro Celaya
39598d8608 Moved templates and templates config to Core module 2016-07-19 18:32:59 +02:00
Alejandro Celaya
923abdf4d2 Added first tests for CLI module 2016-07-19 18:28:21 +02:00
Alejandro Celaya
f917697b8e Added first tests to Rest module 2016-07-19 18:19:05 +02:00
Alejandro Celaya
fe8ef2030f Fixed elements broken on module separation 2016-07-19 18:05:06 +02:00
Alejandro Celaya
ab9c2f728a Created Core module 2016-07-19 18:01:39 +02:00
Alejandro Celaya
ba06ad44bd Created Common module 2016-07-19 17:38:41 +02:00
Alejandro Celaya
8fc88171ee Moved AuthenticationException to Rest module 2016-07-19 17:27:55 +02:00
Alejandro Celaya
7efb3b3a86 Created cli-specific services config file 2016-07-19 17:17:37 +02:00
Alejandro Celaya
170c96b8ef Fixed config files names 2016-07-19 17:12:50 +02:00
Alejandro Celaya
55f954f50f Created Rest module 2016-07-19 17:07:59 +02:00
Alejandro Celaya
95d0beea3c Created CLI module 2016-07-19 16:50:02 +02:00
Alejandro Celaya
5eefaf3071 Added config manager package 2016-07-19 16:30:48 +02:00
Alejandro Celaya
5536d05e99 Added idea folder to gitignore 2016-07-19 16:14:57 +02:00
161 changed files with 4778 additions and 677 deletions

View File

@@ -4,6 +4,10 @@ SHORTENED_URL_SCHEMA=
SHORTENED_URL_HOSTNAME=
SHORTCODE_CHARS=
# Language
DEFAULT_LOCALE=
CLI_LOCALE=
# Database
DB_USER=
DB_PASSWORD=

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.idea
build
composer.lock
vendor/

View File

@@ -8,7 +8,7 @@ branches:
php:
- 5.6
- 7
- hhvm
- 7.1
before_script:
- composer self-update

View File

@@ -1,5 +1,32 @@
## CHANGELOG
### 1.0.0
**Enhancements:**
* [33: Create a command to generate a short code charset by randomizing the default one](https://github.com/acelaya/url-shortener/issues/33)
* [15: Return JSON/HTML responses for errors (4xx and 5xx) based on accept header (content negotiation)](https://github.com/acelaya/url-shortener/issues/15)
* [23: Translate application literals](https://github.com/acelaya/url-shortener/issues/23)
* [21: Allow to filter visits by date range](https://github.com/acelaya/url-shortener/issues/21)
* [22: Save visits locations data on a visit_locations table](https://github.com/acelaya/url-shortener/issues/22)
* [20: Inject cross domain headers in response only if the Origin header is present in the request](https://github.com/acelaya/url-shortener/issues/20)
* [11: Separate code into multiple modules](https://github.com/acelaya/url-shortener/issues/11)
* [18: Group routable middleware in an Action namespace](https://github.com/acelaya/url-shortener/issues/18)
**Tasks**
* [36: Remove hhvm from the CI matrix since it doesn't support array constants and will fail](https://github.com/acelaya/url-shortener/issues/36)
* [4: Installation steps](https://github.com/acelaya/url-shortener/issues/4)
* [6: Remove dependency on expressive helpers package](https://github.com/acelaya/url-shortener/issues/6)
* [30: Replace the "services" first level config entry by "dependencies", in order to fulfill default Expressive name](https://github.com/acelaya/url-shortener/issues/30)
* [12: Improve code coverage](https://github.com/acelaya/url-shortener/issues/12)
* [25: Replace "Middleware" suffix on routable middlewares by "Action"](https://github.com/acelaya/url-shortener/issues/25)
* [19: Update the vendor and app namespace from Acelaya\UrlShortener to Shlinkio\Shlink](https://github.com/acelaya/url-shortener/issues/19)
**Bugs**
* [24: Prevent duplicated shortcodes errors because of the case insensitive behavior on MySQL](https://github.com/acelaya/url-shortener/issues/24)
### 0.2.0
**Enhancements:**

View File

@@ -1,2 +1,9 @@
# url-shortener
# Shlink
[![Build Status](https://travis-ci.org/shlinkio/shlink.svg?branch=master)](https://travis-ci.org/shlinkio/shlink)
[![Code Coverage](https://scrutinizer-ci.com/g/shlinkio/shlink/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/shlinkio/shlink/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
[![Latest Stable Version](https://poser.pugx.org/shlinkio/shlink/v/stable.png)](https://packagist.org/packages/shlinkio/shlink)
[![License](https://poser.pugx.org/shlinkio/shlink/license.png)](https://packagist.org/packages/shlinkio/shlink)
A PHP-based URL shortener application with analytics and management

View File

@@ -2,9 +2,16 @@
<?php
use Interop\Container\ContainerInterface;
use Symfony\Component\Console\Application as CliApp;
use Symfony\Component\Console\Application;
use Zend\I18n\Translator\Translator;
/** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php';
/** @var Translator $translator */
$translator = $container->get('translator');
$translator->setLocale(env('CLI_LOCALE', 'en'));
/** @var Application $app */
$app = $container->get(CliApp::class);
$app->run();

View File

@@ -1,11 +1,12 @@
{
"name": "acelaya/url-shortener",
"name": "shlinkio/shlink",
"type": "project",
"homepage": "https://github.com/acelaya/url-shortener",
"homepage": "http://shlink.io",
"description": "A self-hosted and PHP-based URL shortener application with CLI and REST interfaces",
"license": "MIT",
"authors": [
{
"name": "Alejandro Celaya ALastrué",
"name": "Alejandro Celaya Alastrué",
"homepage": "http://www.alejandrocelaya.com",
"email": "alejandro@alejandrocelaya.com"
}
@@ -13,15 +14,17 @@
"require": {
"php": "^5.6 || ^7.0",
"zendframework/zend-expressive": "^1.0",
"zendframework/zend-expressive-helpers": "^2.0",
"zendframework/zend-expressive-fastroute": "^1.1",
"zendframework/zend-expressive-twigrenderer": "^1.0",
"zendframework/zend-stdlib": "^2.7",
"zendframework/zend-servicemanager": "^3.0",
"zendframework/zend-paginator": "^2.6",
"zendframework/zend-config": "^2.6",
"zendframework/zend-i18n": "^2.7",
"mtymek/expressive-config-manager": "^0.4",
"acelaya/zsm-annotated-services": "^0.2.0",
"doctrine/orm": "^2.5",
"guzzlehttp/guzzle": "^6.2",
"acelaya/zsm-annotated-services": "^0.2.0",
"symfony/console": "^3.0"
},
"require-dev": {
@@ -34,12 +37,21 @@
},
"autoload": {
"psr-4": {
"Acelaya\\UrlShortener\\": "src"
}
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
"Shlinkio\\Shlink\\Common\\": "module/Common/src"
},
"files": [
"module/Common/functions/functions.php"
]
},
"autoload-dev": {
"psr-4": {
"AcelayaTest\\UrlShortener\\": "tests"
"ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test",
"ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test",
"ShlinkioTest\\Shlink\\Core\\": "module/Core/test",
"ShlinkioTest\\Shlink\\Common\\": "module/Common/test"
}
},
"scripts": {

View File

@@ -3,9 +3,9 @@ return [
'database' => [
'driver' => 'pdo_mysql',
'user' => getenv('DB_USER'),
'password' => getenv('DB_PASSWORD'),
'dbname' => getenv('DB_NAME') ?: 'acelaya_url_shortener',
'user' => env('DB_USER'),
'password' => env('DB_PASSWORD'),
'dbname' => env('DB_NAME', 'shlink'),
'charset' => 'utf8',
'driverOptions' => [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'

View File

@@ -0,0 +1,24 @@
<?php
use Shlinkio\Shlink\Common\ErrorHandler\ContentBasedErrorHandler;
use Zend\Expressive;
use Zend\Expressive\Container;
use Zend\Expressive\Router;
use Zend\Expressive\Template;
use Zend\Expressive\Twig;
use Zend\ServiceManager\Factory\InvokableFactory;
return [
'dependencies' => [
'factories' => [
Expressive\Application::class => Container\ApplicationFactory::class,
Router\FastRouteRouter::class => InvokableFactory::class,
Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class,
],
'aliases' => [
Router\RouterInterface::class => Router\FastRouteRouter::class,
'Zend\Expressive\FinalHandler' => ContentBasedErrorHandler::class,
],
],
];

View File

@@ -1,14 +1,13 @@
<?php
use Shlinkio\Shlink\Common\ErrorHandler\ContentBasedErrorHandler;
use Zend\Expressive\Container\WhoopsErrorHandlerFactory;
return [
'services' => [
'dependencies' => [
'invokables' => [
'Zend\Expressive\Whoops' => Whoops\Run::class,
'Zend\Expressive\WhoopsPageHandler' => Whoops\Handler\PrettyPageHandler::class,
],
'factories' => [
'Zend\Expressive\FinalHandler' => Zend\Expressive\Container\WhoopsErrorHandlerFactory::class,
],
],
'whoops' => [
@@ -18,4 +17,12 @@ return [
'ajax_only' => true,
],
],
'error_handler' => [
'plugins' => [
'factories' => [
ContentBasedErrorHandler::DEFAULT_CONTENT => WhoopsErrorHandlerFactory::class,
],
],
],
];

View File

@@ -1,18 +1,9 @@
<?php
use Acelaya\UrlShortener\Middleware;
use Zend\Expressive\Container\ApplicationFactory;
use Zend\Expressive\Helper;
return [
'middleware_pipeline' => [
'always' => [
'middleware' => [
Helper\ServerUrlMiddleware::class,
],
'priority' => 10000,
],
'routing' => [
'middleware' => [
ApplicationFactory::ROUTING_MIDDLEWARE,
@@ -20,27 +11,11 @@ return [
'priority' => 10,
],
'rest' => [
'path' => '/rest',
'middleware' => [
Middleware\CheckAuthenticationMiddleware::class,
Middleware\CrossDomainMiddleware::class,
],
'priority' => 5,
],
'post-routing' => [
'middleware' => [
Helper\UrlHelperMiddleware::class,
ApplicationFactory::DISPATCH_MIDDLEWARE,
],
'priority' => 1,
],
'error' => [
'middleware' => [],
'error' => true,
'priority' => -10000,
],
],
];

View File

@@ -1,9 +0,0 @@
<?php
return [
'rest' => [
'username' => getenv('REST_USER'),
'password' => getenv('REST_PASSWORD'),
],
];

View File

@@ -1,70 +0,0 @@
<?php
use Acelaya\UrlShortener\CLI;
use Acelaya\UrlShortener\Factory\CacheFactory;
use Acelaya\UrlShortener\Factory\EntityManagerFactory;
use Acelaya\UrlShortener\Middleware;
use Acelaya\UrlShortener\Service;
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Doctrine\Common\Cache\Cache;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Console;
use Zend\Expressive;
use Zend\Expressive\Container;
use Zend\Expressive\Helper;
use Zend\Expressive\Router;
use Zend\Expressive\Template;
use Zend\Expressive\Twig;
use Zend\ServiceManager\Factory\InvokableFactory;
return [
'services' => [
'factories' => [
Expressive\Application::class => Container\ApplicationFactory::class,
Console\Application::class => CLI\Factory\ApplicationFactory::class,
// 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\FastRouteRouter::class => InvokableFactory::class,
// View
'Zend\Expressive\FinalHandler' => Container\TemplatedErrorHandlerFactory::class,
Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class,
// Services
EntityManager::class => EntityManagerFactory::class,
GuzzleHttp\Client::class => InvokableFactory::class,
Service\UrlShortener::class => AnnotatedFactory::class,
Service\VisitsTracker::class => AnnotatedFactory::class,
Service\ShortUrlService::class => AnnotatedFactory::class,
Service\RestTokenService::class => AnnotatedFactory::class,
Cache::class => CacheFactory::class,
// Cli commands
CLI\Command\GenerateShortcodeCommand::class => AnnotatedFactory::class,
CLI\Command\ResolveUrlCommand::class => AnnotatedFactory::class,
CLI\Command\ListShortcodesCommand::class => AnnotatedFactory::class,
CLI\Command\GetVisitsCommand::class => AnnotatedFactory::class,
// Middleware
Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\AuthenticateMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\CreateShortcodeMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\ResolveUrlMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\GetVisitsMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\ListShortcodesMiddleware::class => AnnotatedFactory::class,
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class,
],
'aliases' => [
'em' => EntityManager::class,
'httpClient' => GuzzleHttp\Client::class,
Router\RouterInterface::class => Router\FastRouteRouter::class,
AnnotatedFactory::CACHE_SERVICE => Cache::class,
]
],
];

View File

@@ -2,12 +2,6 @@
return [
'templates' => [
'paths' => [
'templates'
],
],
'twig' => [
'cache_dir' => 'data/cache/twig',
'extensions' => [

View File

@@ -0,0 +1,8 @@
<?php
return [
'translator' => [
'locale' => env('DEFAULT_LOCALE', 'en'),
],
];

View File

@@ -1,12 +1,14 @@
<?php
use Shlinkio\Shlink\Core\Service\UrlShortener;
return [
'url_shortener' => [
'domain' => [
'schema' => getenv('SHORTENED_URL_SCHEMA') ?: 'http',
'hostname' => getenv('SHORTENED_URL_HOSTNAME'),
'schema' => env('SHORTENED_URL_SCHEMA', 'http'),
'hostname' => env('SHORTENED_URL_HOSTNAME'),
],
'shortcode_chars' => getenv('SHORTCODE_CHARS'),
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS),
],
];

View File

@@ -1,14 +1,8 @@
<?php
return [
'debug' => false,
'config_cache_enabled' => true,
'config_cache_enabled' => false,
'zend-expressive' => [
'error_handler' => [
'template_404' => 'error/404.html.twig',
'template_error' => 'error/error.html.twig',
],
],
];

View File

@@ -4,7 +4,7 @@ use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Interop\Container\ContainerInterface;
/** @var ContainerInterface $container */
$container = include __DIR__ . '/config/container.php';
$container = include __DIR__ . '/container.php';
/** @var EntityManager $em */
$em = $container->get(EntityManager::class);

View File

@@ -1,6 +1,10 @@
<?php
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\Glob;
use Shlinkio\Shlink\CLI;
use Shlinkio\Shlink\Common;
use Shlinkio\Shlink\Core;
use Shlinkio\Shlink\Rest;
use Zend\Expressive\ConfigManager\ConfigManager;
use Zend\Expressive\ConfigManager\ZendConfigProvider;
/**
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
@@ -11,22 +15,10 @@ use Zend\Stdlib\Glob;
* Obviously, if you use closures in your config you can't cache it.
*/
$cachedConfigFile = 'data/cache/app_config.php';
$config = [];
if (is_file($cachedConfigFile)) {
// Try to load the cached config
$config = include $cachedConfigFile;
} else {
// Load configuration from autoload path
foreach (Glob::glob('config/autoload/{{,*.}global,{,*.}local}.php', Glob::GLOB_BRACE) as $file) {
$config = ArrayUtils::merge($config, include $file);
}
// Cache config if enabled
if (isset($config['config_cache_enabled']) && $config['config_cache_enabled'] === true) {
file_put_contents($cachedConfigFile, '<?php return ' . var_export($config, true) . ';');
}
}
return $config;
return (new ConfigManager([
Common\ConfigProvider::class,
Core\ConfigProvider::class,
CLI\ConfigProvider::class,
Rest\ConfigProvider::class,
new ZendConfigProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
], 'data/cache/app_config.php'))->getMergedConfig();

View File

@@ -16,6 +16,6 @@ if (class_exists(Dotenv::class)) {
// Build container
$config = require __DIR__ . '/config.php';
$container = new ServiceManager($config['services']);
$container = new ServiceManager($config['dependencies']);
$container->setService('config', $config);
return $container;

19
data/docs/installation.md Normal file
View File

@@ -0,0 +1,19 @@
### Installation steps
- Define ENV vars in apache or nginx:
- SHORTENED_URL_SCHEMA: http|https
- SHORTENED_URL_HOSTNAME: Short domain
- SHORTCODE_CHARS: The char set used to generate short codes (defaults to **123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ**, but a new one can be generated with the `config:generate-charset` command)
- DB_USER: MySQL database user
- DB_PASSWORD: MySQL database password
- REST_USER: Username for REST authentication
- REST_PASSWORD: Password for REST authentication
- DB_NAME: MySQL database name (defaults to **shlink**)
- DEFAULT_LOCALE: Language in which web requests (browser and REST) will be returned if no `Accept-Language` header is sent (defaults to **en**)
- CLI_LOCALE: Language in which console command messages will be displayed (defaults to **en**)
- Create database (`vendor/bin/doctrine orm:schema-tool:create`)
- Add write permissions to `data` directory
- Create doctrine proxies (`vendor/bin/doctrine orm:generate-proxies`)
- Create symlink to bin/cli as `shlink` in /usr/local/bin (linux only. Optional)
Supported languages: es and en

View File

@@ -13,7 +13,15 @@ Statuses:
## Authentication
[TODO]
Once you have called to the authentication endpoint for the first time (see below) yopu will get an authentication token.
You will have to send that token in the `X-Auth-Token` header on any later request or you will get an authentication error.
## Language
In order to set the application language, you have to pass it by using the `Accept-Language` header.
If not provided or provided language is not supported, english (en_US) will be used.
## Endpoints
@@ -222,9 +230,12 @@ Posible errors:
**REQUEST**
* `GET` -> `/rest/visits/{shortCode}`
* `GET` -> `/rest/short-codes/{shortCode}/visits`
* Route params:
* shortCode: `string` -> The shortCode from which we eant to get the visits.
* Query params:
* startDate: `string` -> If provided, only visits older that this date will be returned
* endDate: `string` -> If provided, only visits newer that this date will be returned
* Headers:
* X-Auth-Token: `string` -> The token provided in the authentication request

View File

@@ -1,5 +1,5 @@
<?php
use Acelaya\UrlShortener\CLI\Command;
use Shlinkio\Shlink\CLI\Command;
return [
@@ -9,6 +9,8 @@ return [
Command\ResolveUrlCommand::class,
Command\ListShortcodesCommand::class,
Command\GetVisitsCommand::class,
Command\ProcessVisitsCommand::class,
Command\Config\GenerateCharsetCommand::class,
]
],

View File

@@ -0,0 +1,23 @@
<?php
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Shlinkio\Shlink\CLI\Command;
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
use Symfony\Component\Console\Application;
return [
'dependencies' => [
'factories' => [
Application::class => ApplicationFactory::class,
Command\GenerateShortcodeCommand::class => AnnotatedFactory::class,
Command\ResolveUrlCommand::class => AnnotatedFactory::class,
Command\ListShortcodesCommand::class => AnnotatedFactory::class,
Command\GetVisitsCommand::class => AnnotatedFactory::class,
Command\ProcessVisitsCommand::class => AnnotatedFactory::class,
Command\ProcessVisitsCommand::class => AnnotatedFactory::class,
Command\Config\GenerateCharsetCommand::class => AnnotatedFactory::class,
],
],
];

View File

@@ -0,0 +1,14 @@
<?php
return [
'translator' => [
'translation_file_patterns' => [
[
'type' => 'gettext',
'base_dir' => __DIR__ . '/../lang',
'pattern' => '%s.mo',
],
],
],
];

BIN
module/CLI/lang/es.mo Normal file

Binary file not shown.

147
module/CLI/lang/es.po Normal file
View File

@@ -0,0 +1,147 @@
msgid ""
msgstr ""
"Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2016-08-01 21:21+0200\n"
"PO-Revision-Date: 2016-08-01 21:22+0200\n"
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
"Language-Team: \n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.7.1\n"
"X-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: translate;translatePlural\n"
"X-Poedit-SearchPath-0: src\n"
"X-Poedit-SearchPath-1: config\n"
#, php-format
msgid ""
"Generates a character set sample just by shuffling the default one, \"%s\". "
"Then it can be set in the SHORTCODE_CHARS environment variable"
msgstr ""
"Genera un grupo de caracteres simplemente mexclando el grupo por defecto \"%s"
"\". Después puede ser utilizado en la variable de entrono SHORTCODE_CHARS"
msgid "Character set:"
msgstr "Grupo de caracteres:"
#, fuzzy
msgid "Generates a short code for provided URL and returns the short URL"
msgstr ""
"Genera un código corto para la URL proporcionada y devuelve la URL acortada"
msgid "The long URL to parse"
msgstr "La URL larga a procesar"
msgid "A long URL was not provided. Which URL do you want to shorten?:"
msgstr "No se ha proporcionado una URL larga. ¿Qué URL deseas acortar?"
msgid "A URL was not provided!"
msgstr "¡No se ha proporcionado una URL!"
msgid "Processed URL:"
msgstr "URL procesada:"
msgid "Generated URL:"
msgstr "URL generada:"
#, php-format
msgid "Provided URL \"%s\" is invalid. Try with a different one."
msgstr "La URL proporcionada \"%s\" e inválida. Prueba con una diferente."
msgid "Returns the detailed visits information for provided short code"
msgstr ""
"Devuelve la información detallada de visitas para el código corto "
"proporcionado"
msgid "The short code which visits we want to get"
msgstr "El código corto del cual queremos obtener las visitas"
msgid "Allows to filter visits, returning only those older than start date"
msgstr ""
"Permite filtrar las visitas, devolviendo sólo aquellas más antiguas que "
"startDate"
msgid "Allows to filter visits, returning only those newer than end date"
msgstr ""
"Permite filtrar las visitas, devolviendo sólo aquellas más nuevas que endDate"
msgid "A short code was not provided. Which short code do you want to use?:"
msgstr "No se prporcionó un código corto. ¿Qué código corto deseas usar?"
msgid "Referer"
msgstr "Origen"
msgid "Date"
msgstr "Fecha"
msgid "Remote Address"
msgstr "Dirección remota"
msgid "User agent"
msgstr "Agente de usuario"
msgid "List all short URLs"
msgstr "Listar todas las URLs cortas"
#, php-format
msgid "The first page to list (%s items per page)"
msgstr "La primera página a listar (%s elementos por página)"
msgid "Short code"
msgstr "Código corto"
msgid "Original URL"
msgstr "URL original"
msgid "Date created"
msgstr "Fecha de creación"
msgid "Visits count"
msgstr "Número de visitas"
msgid "You have reached last page"
msgstr "Has alcanzado la última página"
msgid "Continue with page"
msgstr "Continuar con la página"
msgid "Processes visits where location is not set yet"
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
msgid "Processing IP"
msgstr "Procesando IP"
msgid "Ignored localhost address"
msgstr "Ignorada IP de localhost"
#, php-format
msgid "Address located at \"%s\""
msgstr "Dirección localizada en \"%s\""
msgid "Finished processing all IPs"
msgstr "Finalizado el procesado de todas las IPs"
msgid "Returns the long URL behind a short code"
msgstr "Devuelve la URL larga detrás de un código corto"
msgid "The short code to parse"
msgstr "El código corto a convertir"
msgid "A short code was not provided. Which short code do you want to parse?:"
msgstr ""
"No se proporcionó un código corto. ¿Qué código corto quieres convertir?"
#, php-format
msgid "No URL found for short code \"%s\""
msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
msgid "Long URL:"
msgstr "URL larga:"
#, php-format
msgid "Provided short code \"%s\" has an invalid format."
msgstr "El código corto proporcionado \"%s\" tiene un formato inválido."

View File

@@ -0,0 +1,44 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Config;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Zend\I18n\Translator\TranslatorInterface;
class GenerateCharsetCommand extends Command
{
/**
* @var TranslatorInterface
*/
private $translator;
/**
* GenerateCharsetCommand constructor.
* @param TranslatorInterface $translator
*
* @Inject({"translator"})
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('config:generate-charset')
->setDescription(sprintf($this->translator->translate(
'Generates a character set sample just by shuffling the default one, "%s". '
. 'Then it can be set in the SHORTCODE_CHARS environment variable'
), UrlShortener::DEFAULT_CHARS));
}
public function execute(InputInterface $input, OutputInterface $output)
{
$charSet = str_shuffle(UrlShortener::DEFAULT_CHARS);
$output->writeln($this->translator->translate('Character set:') . sprintf(' <info>%s</info>', $charSet));
}
}

View File

@@ -1,18 +1,18 @@
<?php
namespace Acelaya\UrlShortener\CLI\Command;
namespace Shlinkio\Shlink\CLI\Command;
use Acelaya\UrlShortener\Exception\InvalidUrlException;
use Acelaya\UrlShortener\Service\UrlShortener;
use Acelaya\UrlShortener\Service\UrlShortenerInterface;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
use Zend\Diactoros\Uri;
use Zend\I18n\Translator\TranslatorInterface;
class GenerateShortcodeCommand extends Command
{
@@ -24,26 +24,37 @@ class GenerateShortcodeCommand extends Command
* @var array
*/
private $domainConfig;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* GenerateShortcodeCommand constructor.
* @param UrlShortenerInterface|UrlShortener $urlShortener
* @param TranslatorInterface $translator
* @param array $domainConfig
*
* @Inject({UrlShortener::class, "config.url_shortener.domain"})
* @Inject({UrlShortener::class, "translator", "config.url_shortener.domain"})
*/
public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig)
{
parent::__construct(null);
public function __construct(
UrlShortenerInterface $urlShortener,
TranslatorInterface $translator,
array $domainConfig
) {
$this->urlShortener = $urlShortener;
$this->translator = $translator;
$this->domainConfig = $domainConfig;
parent::__construct(null);
}
public function configure()
{
$this->setName('shortcode:generate')
->setDescription('Generates a shortcode for provided URL and returns the short URL')
->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse');
->setDescription(
$this->translator->translate('Generates a short code for provided URL and returns the short URL')
)
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'));
}
public function interact(InputInterface $input, OutputInterface $output)
@@ -55,9 +66,10 @@ class GenerateShortcodeCommand extends Command
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new Question(
'<question>A long URL was not provided. Which URL do you want to shorten?:</question> '
);
$question = new Question(sprintf(
'<question>%s</question> ',
$this->translator->translate('A long URL was not provided. Which URL do you want to shorten?:')
));
$longUrl = $helper->ask($input, $output, $question);
if (! empty($longUrl)) {
@@ -71,23 +83,26 @@ class GenerateShortcodeCommand extends Command
try {
if (! isset($longUrl)) {
$output->writeln('<error>A URL was not provided!</error>');
$output->writeln(sprintf('<error>%s</error>', $this->translator->translate('A URL was not provided!')));
return;
}
$shortcode = $this->urlShortener->urlToShortCode(new Uri($longUrl));
$shortUrl = (new Uri())->withPath($shortcode)
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl));
$shortUrl = (new Uri())->withPath($shortCode)
->withScheme($this->domainConfig['schema'])
->withHost($this->domainConfig['hostname']);
$output->writeln([
sprintf('Processed URL <info>%s</info>', $longUrl),
sprintf('Generated URL <info>%s</info>', $shortUrl),
sprintf('%s <info>%s</info>', $this->translator->translate('Processed URL:'), $longUrl),
sprintf('%s <info>%s</info>', $this->translator->translate('Generated URL:'), $shortUrl),
]);
} catch (InvalidUrlException $e) {
$output->writeln(
sprintf('<error>Provided URL "%s" is invalid. Try with a different one.</error>', $longUrl)
);
$output->writeln(sprintf(
'<error>' . $this->translator->translate(
'Provided URL "%s" is invalid. Try with a different one.'
) . '</error>',
$longUrl
));
}
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Shlinkio\Shlink\CLI\Command;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Service\VisitsTracker;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Zend\I18n\Translator\TranslatorInterface;
class GetVisitsCommand extends Command
{
/**
* @var VisitsTrackerInterface
*/
private $visitsTracker;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* GetVisitsCommand constructor.
* @param VisitsTrackerInterface|VisitsTracker $visitsTracker
* @param TranslatorInterface $translator
*
* @Inject({VisitsTracker::class, "translator"})
*/
public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator)
{
$this->visitsTracker = $visitsTracker;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('shortcode:visits')
->setDescription(
$this->translator->translate('Returns the detailed visits information for provided short code')
)
->addArgument(
'shortCode',
InputArgument::REQUIRED,
$this->translator->translate('The short code which visits we want to get')
)
->addOption(
'startDate',
's',
InputOption::VALUE_OPTIONAL,
$this->translator->translate('Allows to filter visits, returning only those older than start date')
)
->addOption(
'endDate',
'e',
InputOption::VALUE_OPTIONAL,
$this->translator->translate('Allows to filter visits, returning only those newer than end date')
);
}
public function interact(InputInterface $input, OutputInterface $output)
{
$shortCode = $input->getArgument('shortCode');
if (! empty($shortCode)) {
return;
}
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new Question(sprintf(
'<question>%s</question> ',
$this->translator->translate('A short code was not provided. Which short code do you want to use?:')
));
$shortCode = $helper->ask($input, $output, $question);
if (! empty($shortCode)) {
$input->setArgument('shortCode', $shortCode);
}
}
public function execute(InputInterface $input, OutputInterface $output)
{
$shortCode = $input->getArgument('shortCode');
$startDate = $this->getDateOption($input, 'startDate');
$endDate = $this->getDateOption($input, 'endDate');
$visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate));
$table = new Table($output);
$table->setHeaders([
$this->translator->translate('Referer'),
$this->translator->translate('Date'),
$this->translator->translate('Remote Address'),
$this->translator->translate('User agent'),
]);
foreach ($visits as $row) {
$rowData = $row->jsonSerialize();
// Unset location info
unset($rowData['visitLocation']);
$table->addRow(array_values($rowData));
}
$table->render();
}
protected function getDateOption(InputInterface $input, $key)
{
$value = $input->getOption($key);
if (isset($value)) {
$value = new \DateTime($value);
}
return $value;
}
}

View File

@@ -1,11 +1,11 @@
<?php
namespace Acelaya\UrlShortener\CLI\Command;
namespace Shlinkio\Shlink\CLI\Command;
use Acelaya\UrlShortener\Paginator\Adapter\PaginableRepositoryAdapter;
use Acelaya\UrlShortener\Paginator\Util\PaginatorUtilsTrait;
use Acelaya\UrlShortener\Service\ShortUrlService;
use Acelaya\UrlShortener\Service\ShortUrlServiceInterface;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Helper\Table;
@@ -13,6 +13,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Zend\I18n\Translator\TranslatorInterface;
class ListShortcodesCommand extends Command
{
@@ -22,28 +23,37 @@ class ListShortcodesCommand extends Command
* @var ShortUrlServiceInterface
*/
private $shortUrlService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* ListShortcodesCommand constructor.
* @param ShortUrlServiceInterface|ShortUrlService $shortUrlService
* @param TranslatorInterface $translator
*
* @Inject({ShortUrlService::class})
* @Inject({ShortUrlService::class, "translator"})
*/
public function __construct(ShortUrlServiceInterface $shortUrlService)
public function __construct(ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator)
{
parent::__construct(null);
$this->shortUrlService = $shortUrlService;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('shortcode:list')
->setDescription('List all short URLs')
->setDescription($this->translator->translate('List all short URLs'))
->addOption(
'page',
'p',
InputOption::VALUE_OPTIONAL,
sprintf('The first page to list (%s items per page)', PaginableRepositoryAdapter::ITEMS_PER_PAGE),
sprintf(
$this->translator->translate('The first page to list (%s items per page)'),
PaginableRepositoryAdapter::ITEMS_PER_PAGE
),
1
);
}
@@ -59,10 +69,10 @@ class ListShortcodesCommand extends Command
$page++;
$table = new Table($output);
$table->setHeaders([
'Short code',
'Original URL',
'Date created',
'Visits count',
$this->translator->translate('Short code'),
$this->translator->translate('Original URL'),
$this->translator->translate('Date created'),
$this->translator->translate('Visits count'),
]);
foreach ($result as $row) {
@@ -72,10 +82,14 @@ class ListShortcodesCommand extends Command
if ($this->isLastPage($result)) {
$continue = false;
$output->writeln('<info>You have reached last page</info>');
$output->writeln(
sprintf('<info>%s</info>', $this->translator->translate('You have reached last page'))
);
} else {
$continue = $helper->ask($input, $output, new ConfirmationQuestion(
sprintf('<question>Continue with page <bg=cyan;options=bold>%s</>? (y/N)</question> ', $page),
sprintf('<question>' . $this->translator->translate(
'Continue with page'
) . ' <bg=cyan;options=bold>%s</>? (y/N)</question> ', $page),
false
));
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Shlinkio\Shlink\CLI\Command;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
use Shlinkio\Shlink\Common\Service\IpLocationResolverInterface;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Service\VisitService;
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Zend\I18n\Translator\TranslatorInterface;
class ProcessVisitsCommand extends Command
{
const LOCALHOST = '127.0.0.1';
/**
* @var VisitServiceInterface
*/
private $visitService;
/**
* @var IpLocationResolverInterface
*/
private $ipLocationResolver;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* ProcessVisitsCommand constructor.
* @param VisitServiceInterface|VisitService $visitService
* @param IpLocationResolverInterface|IpLocationResolver $ipLocationResolver
* @param TranslatorInterface $translator
*
* @Inject({VisitService::class, IpLocationResolver::class, "translator"})
*/
public function __construct(
VisitServiceInterface $visitService,
IpLocationResolverInterface $ipLocationResolver,
TranslatorInterface $translator
) {
$this->visitService = $visitService;
$this->ipLocationResolver = $ipLocationResolver;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('visit:process')
->setDescription(
$this->translator->translate('Processes visits where location is not set yet')
);
}
public function execute(InputInterface $input, OutputInterface $output)
{
$visits = $this->visitService->getUnlocatedVisits();
foreach ($visits as $visit) {
$ipAddr = $visit->getRemoteAddr();
$output->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
if ($ipAddr === self::LOCALHOST) {
$output->writeln(
sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
);
continue;
}
try {
$result = $this->ipLocationResolver->resolveIpLocation($ipAddr);
$location = new VisitLocation();
$location->exchangeArray($result);
$visit->setVisitLocation($location);
$this->visitService->saveVisit($visit);
$output->writeln(sprintf(
' (' . $this->translator->translate('Address located at "%s"') . ')',
$location->getCityName()
));
} catch (WrongIpException $e) {
continue;
}
}
$output->writeln($this->translator->translate('Finished processing all IPs'));
}
}

View File

@@ -1,16 +1,17 @@
<?php
namespace Acelaya\UrlShortener\CLI\Command;
namespace Shlinkio\Shlink\CLI\Command;
use Acelaya\UrlShortener\Exception\InvalidShortCodeException;
use Acelaya\UrlShortener\Service\UrlShortener;
use Acelaya\UrlShortener\Service\UrlShortenerInterface;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Zend\I18n\Translator\TranslatorInterface;
class ResolveUrlCommand extends Command
{
@@ -18,24 +19,34 @@ class ResolveUrlCommand extends Command
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* ResolveUrlCommand constructor.
* @param UrlShortenerInterface|UrlShortener $urlShortener
* @param TranslatorInterface $translator
*
* @Inject({UrlShortener::class})
* @Inject({UrlShortener::class, "translator"})
*/
public function __construct(UrlShortenerInterface $urlShortener)
public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator)
{
parent::__construct(null);
$this->urlShortener = $urlShortener;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('shortcode:parse')
->setDescription('Returns the long URL behind a short code')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse');
->setDescription($this->translator->translate('Returns the long URL behind a short code'))
->addArgument(
'shortCode',
InputArgument::REQUIRED,
$this->translator->translate('The short code to parse')
);
}
public function interact(InputInterface $input, OutputInterface $output)
@@ -47,9 +58,10 @@ class ResolveUrlCommand extends Command
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new Question(
'<question>A short code was not provided. Which short code do you want to parse?:</question> '
);
$question = new Question(sprintf(
'<question>%s</question> ',
$this->translator->translate('A short code was not provided. Which short code do you want to parse?:')
));
$shortCode = $helper->ask($input, $output, $question);
if (! empty($shortCode)) {
@@ -64,15 +76,18 @@ class ResolveUrlCommand extends Command
try {
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
if (! isset($longUrl)) {
$output->writeln(sprintf('<error>No URL found for short code "%s"</error>', $shortCode));
$output->writeln(sprintf(
'<error>' . $this->translator->translate('No URL found for short code "%s"') . '</error>',
$shortCode
));
return;
}
$output->writeln(sprintf('Long URL <info>%s</info>', $longUrl));
$output->writeln(sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $longUrl));
} catch (InvalidShortCodeException $e) {
$output->writeln(
sprintf('<error>Provided short code "%s" has an invalid format.</error>', $shortCode)
);
$output->writeln(sprintf('<error>' . $this->translator->translate(
'Provided short code "%s" has an invalid format.'
) . '</error>', $shortCode));
}
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Shlinkio\Shlink\CLI;
use Zend\Config\Factory;
use Zend\Stdlib\Glob;
class ConfigProvider
{
public function __invoke()
{
return Factory::fromFiles(Glob::glob(__DIR__ . '/../config/{,*.}config.php', Glob::GLOB_BRACE));
}
}

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\CLI\Factory;
namespace Shlinkio\Shlink\CLI\Factory;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
@@ -25,7 +25,7 @@ class ApplicationFactory implements FactoryInterface
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->get('config')['cli'];
$app = new CliApp();
$app = new CliApp('Shlink', '1.0.0');
$commands = isset($config['commands']) ? $config['commands'] : [];
foreach ($commands as $command) {

View File

@@ -0,0 +1,56 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Config;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class GenerateCharsetCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
public function setUp()
{
$command = new GenerateCharsetCommand(Translator::factory([]));
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function charactersAreGeneratedFromDefault()
{
$prefix = 'Character set: ';
$prefixLength = strlen($prefix);
$this->commandTester->execute([
'command' => 'config:generate-charset',
]);
$output = $this->commandTester->getDisplay();
// Both default character set and the new one should have the same length
$this->assertEquals($prefixLength + strlen(UrlShortener::DEFAULT_CHARS) + 1, strlen($output));
// Both default character set and the new one should have the same characters
$charset = substr($output, $prefixLength, strlen(UrlShortener::DEFAULT_CHARS));
$orderedDefault = $this->orderStringLetters(UrlShortener::DEFAULT_CHARS);
$orderedCharset = $this->orderStringLetters($charset);
$this->assertEquals($orderedDefault, $orderedCharset);
}
protected function orderStringLetters($string)
{
$letters = str_split($string);
sort($letters);
return implode('', $letters);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\GenerateShortcodeCommand;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class GenerateShortcodeCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $urlShortener;
public function setUp()
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$command = new GenerateShortcodeCommand($this->urlShortener->reveal(), Translator::factory([]), [
'schema' => 'http',
'hostname' => 'foo.com'
]);
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function properShortCodeIsCreatedIfLongUrlIsCorrect()
{
$this->urlShortener->urlToShortCode(Argument::any())->willReturn('abc123')
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:generate',
'longUrl' => 'http://domain.com/foo/bar'
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(strpos($output, 'http://foo.com/abc123') > 0);
}
/**
* @test
*/
public function exceptionWhileParsingLongUrlOutputsError()
{
$this->urlShortener->urlToShortCode(Argument::any())->willThrow(new InvalidUrlException())
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:generate',
'longUrl' => 'http://domain.com/invalid'
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(
strpos($output, 'Provided URL "http://domain.com/invalid" is invalid. Try with a different one.') === 0
);
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\GetVisitsCommand;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class GetVisitsCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $visitsTracker;
public function setUp()
{
$this->visitsTracker = $this->prophesize(VisitsTrackerInterface::class);
$command = new GetVisitsCommand($this->visitsTracker->reveal(), Translator::factory([]));
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function noDateFlagsTriesToListWithoutDateRange()
{
$shortCode = 'abc123';
$this->visitsTracker->info($shortCode, new DateRange(null, null))->willReturn([])
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:visits',
'shortCode' => $shortCode,
]);
}
/**
* @test
*/
public function providingDateFlagsTheListGetsFiltered()
{
$shortCode = 'abc123';
$startDate = '2016-01-01';
$endDate = '2016-02-01';
$this->visitsTracker->info($shortCode, new DateRange(new \DateTime($startDate), new \DateTime($endDate)))
->willReturn([])
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:visits',
'shortCode' => $shortCode,
'--startDate' => $startDate,
'--endDate' => $endDate,
]);
}
/**
* @test
*/
public function outputIsProperlyGenerated()
{
$shortCode = 'abc123';
$this->visitsTracker->info($shortCode, Argument::any())->willReturn([
(new Visit())->setReferer('foo')
->setRemoteAddr('1.2.3.4')
->setUserAgent('bar'),
])->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:visits',
'shortCode' => $shortCode,
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(strpos($output, 'foo') > 0);
$this->assertTrue(strpos($output, '1.2.3.4') > 0);
$this->assertTrue(strpos($output, 'bar') > 0);
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ListShortcodesCommand;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
use Zend\Paginator\Adapter\ArrayAdapter;
use Zend\Paginator\Paginator;
class ListShortcodesCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var QuestionHelper
*/
protected $questionHelper;
/**
* @var ObjectProphecy
*/
protected $shortUrlService;
public function setUp()
{
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
$app = new Application();
$command = new ListShortcodesCommand($this->shortUrlService->reveal(), Translator::factory([]));
$app->add($command);
$this->questionHelper = $command->getHelper('question');
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function noInputCallsListJustOnce()
{
$this->questionHelper->setInputStream($this->getInputStream('\n'));
$this->shortUrlService->listShortUrls(1)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledTimes(1);
$this->commandTester->execute(['command' => 'shortcode:list']);
}
/**
* @test
*/
public function loadingMorePagesCallsListMoreTimes()
{
// The paginator will return more than one page for the first 3 times
$data = [];
for ($i = 0; $i < 30; $i++) {
$data[] = new ShortUrl();
}
$data = array_chunk($data, 11);
$questionHelper = $this->questionHelper;
$that = $this;
$this->shortUrlService->listShortUrls(Argument::any())->will(function () use (&$data, $questionHelper, $that) {
$questionHelper->setInputStream($that->getInputStream('y'));
return new Paginator(new ArrayAdapter(array_shift($data)));
})->shouldBeCalledTimes(3);
$this->commandTester->execute(['command' => 'shortcode:list']);
}
/**
* @test
*/
public function havingMorePagesButAnsweringNoCallsListJustOnce()
{
// The paginator will return more than one page
$data = [];
for ($i = 0; $i < 30; $i++) {
$data[] = new ShortUrl();
}
$this->questionHelper->setInputStream($this->getInputStream('n'));
$this->shortUrlService->listShortUrls(Argument::any())->willReturn(new Paginator(new ArrayAdapter($data)))
->shouldBeCalledTimes(1);
$this->commandTester->execute(['command' => 'shortcode:list']);
}
/**
* @test
*/
public function passingPageWillMakeListStartOnThatPage()
{
$page = 5;
$this->questionHelper->setInputStream($this->getInputStream('\n'));
$this->shortUrlService->listShortUrls($page)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:list',
'--page' => $page,
]);
}
protected function getInputStream($inputData)
{
$stream = fopen('php://memory', 'r+', false);
fputs($stream, $inputData);
rewind($stream);
return $stream;
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ProcessVisitsCommand;
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Service\VisitService;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class ProcessVisitsCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $visitService;
/**
* @var ObjectProphecy
*/
protected $ipResolver;
public function setUp()
{
$this->visitService = $this->prophesize(VisitService::class);
$this->ipResolver = $this->prophesize(IpLocationResolver::class);
$command = new ProcessVisitsCommand(
$this->visitService->reveal(),
$this->ipResolver->reveal(),
Translator::factory([])
);
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function allReturnedVisitsIpsAreProcessed()
{
$visits = [
(new Visit())->setRemoteAddr('1.2.3.4'),
(new Visit())->setRemoteAddr('4.3.2.1'),
(new Visit())->setRemoteAddr('12.34.56.78'),
];
$this->visitService->getUnlocatedVisits()->willReturn($visits)
->shouldBeCalledTimes(1);
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits));
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
->shouldBeCalledTimes(count($visits));
$this->commandTester->execute([
'command' => 'visit:process',
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(strpos($output, 'Processing IP 1.2.3.4') === 0);
$this->assertTrue(strpos($output, 'Processing IP 4.3.2.1') > 0);
$this->assertTrue(strpos($output, 'Processing IP 12.34.56.78') > 0);
}
/**
* @test
*/
public function localhostAddressIsIgnored()
{
$visits = [
(new Visit())->setRemoteAddr('1.2.3.4'),
(new Visit())->setRemoteAddr('4.3.2.1'),
(new Visit())->setRemoteAddr('12.34.56.78'),
(new Visit())->setRemoteAddr('127.0.0.1'),
(new Visit())->setRemoteAddr('127.0.0.1'),
];
$this->visitService->getUnlocatedVisits()->willReturn($visits)
->shouldBeCalledTimes(1);
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits) - 2);
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
->shouldBeCalledTimes(count($visits) - 2);
$this->commandTester->execute([
'command' => 'visit:process',
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(strpos($output, 'Ignored localhost address') > 0);
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ResolveUrlCommand;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class ResolveUrlCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $urlShortener;
public function setUp()
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$command = new ResolveUrlCommand($this->urlShortener->reveal(), Translator::factory([]));
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function correctShortCodeResolvesUrl()
{
$shortCode = 'abc123';
$expectedUrl = 'http://domain.com/foo/bar';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($expectedUrl)
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:parse',
'shortCode' => $shortCode,
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output);
}
/**
* @test
*/
public function incorrectShortCodeOutputsErrorMessage()
{
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null)
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:parse',
'shortCode' => $shortCode,
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals('No URL found for short code "' . $shortCode . '"' . PHP_EOL, $output);
}
/**
* @test
*/
public function wrongShortCodeFormatOutputsErrorMessage()
{
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(new InvalidShortCodeException())
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:parse',
'shortCode' => $shortCode,
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals('Provided short code "' . $shortCode . '" has an invalid format.' . PHP_EOL, $output);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace ShlinkioTest\Shlink\CLI;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\CLI\ConfigProvider;
class ConfigProviderTest extends TestCase
{
/**
* @var ConfigProvider
*/
protected $configProvider;
public function setUp()
{
$this->configProvider = new ConfigProvider();
}
/**
* @test
*/
public function confiIsProperlyReturned()
{
$config = $this->configProvider->__invoke();
$this->assertArrayHasKey('cli', $config);
$this->assertArrayHasKey('dependencies', $config);
$this->assertArrayHasKey('translator', $config);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Factory;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Zend\ServiceManager\ServiceManager;
class ApplicationFactoryTest extends TestCase
{
/**
* @var ApplicationFactory
*/
protected $factory;
public function setUp()
{
$this->factory = new ApplicationFactory();
}
/**
* @test
*/
public function serviceIsCreated()
{
$instance = $this->factory->__invoke($this->createServiceManager(), '');
$this->assertInstanceOf(Application::class, $instance);
}
/**
* @test
*/
public function allCommandsWhichAreServicesAreAdded()
{
$sm = $this->createServiceManager([
'commands' => [
'foo',
'bar',
'baz',
],
]);
$sm->setService('foo', $this->prophesize(Command::class)->reveal());
$sm->setService('baz', $this->prophesize(Command::class)->reveal());
/** @var Application $instance */
$instance = $this->factory->__invoke($sm, '');
$this->assertInstanceOf(Application::class, $instance);
$this->assertCount(2, $instance->all());
}
protected function createServiceManager($config = [])
{
return new ServiceManager(['services' => [
'config' => [
'cli' => $config,
],
]]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Doctrine\Common\Cache\Cache;
use Doctrine\ORM\EntityManager;
use Shlinkio\Shlink\Common\ErrorHandler;
use Shlinkio\Shlink\Common\Factory\CacheFactory;
use Shlinkio\Shlink\Common\Factory\EntityManagerFactory;
use Shlinkio\Shlink\Common\Factory\TranslatorFactory;
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\Factory\InvokableFactory;
return [
'dependencies' => [
'factories' => [
EntityManager::class => EntityManagerFactory::class,
GuzzleHttp\Client::class => InvokableFactory::class,
Cache::class => CacheFactory::class,
IpLocationResolver::class => AnnotatedFactory::class,
Translator::class => TranslatorFactory::class,
TranslatorExtension::class => AnnotatedFactory::class,
LocaleMiddleware::class => AnnotatedFactory::class,
ErrorHandler\ContentBasedErrorHandler::class => AnnotatedFactory::class,
ErrorHandler\ErrorHandlerManager::class => ErrorHandler\ErrorHandlerManagerFactory::class,
],
'aliases' => [
'em' => EntityManager::class,
'httpClient' => GuzzleHttp\Client::class,
'translator' => Translator::class,
AnnotatedFactory::CACHE_SERVICE => Cache::class,
],
],
];

View File

@@ -0,0 +1,22 @@
<?php
use Shlinkio\Shlink\Common\ErrorHandler\ContentBasedErrorHandler;
use Zend\Expressive\Container\TemplatedErrorHandlerFactory;
use Zend\Stratigility\FinalHandler;
return [
'error_handler' => [
'plugins' => [
'invokables' => [
'text/plain' => FinalHandler::class,
],
'factories' => [
ContentBasedErrorHandler::DEFAULT_CONTENT => TemplatedErrorHandlerFactory::class,
],
'aliases' => [
'application/xhtml+xml' => ContentBasedErrorHandler::DEFAULT_CONTENT,
],
],
],
];

View File

@@ -0,0 +1,15 @@
<?php
use Shlinkio\Shlink\Common\Middleware;
return [
'middleware_pipeline' => [
'pre-routing' => [
'middleware' => [
Middleware\LocaleMiddleware::class,
],
'priority' => 5,
],
],
];

View File

@@ -0,0 +1,12 @@
<?php
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
return [
'twig' => [
'extensions' => [
TranslatorExtension::class,
],
],
];

View File

@@ -0,0 +1,36 @@
<?php
if (! function_exists('env')) {
/**
* Gets the value of an environment variable. Supports boolean, empty and null.
* This is basically Laravel's env helper
*
* @param string $key
* @param mixed $default
* @return mixed
* @link https://github.com/laravel/framework/blob/5.2/src/Illuminate/Foundation/helpers.php#L369
*/
function env($key, $default = null)
{
$value = getenv($key);
if ($value === false) {
return $default;
}
switch (strtolower($value)) {
case 'true':
case '(true)':
return true;
case 'false':
case '(false)':
return false;
case 'empty':
case '(empty)':
return '';
case 'null':
case '(null)':
return null;
}
return trim($value);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Shlinkio\Shlink\Common;
use Zend\Config\Factory;
use Zend\Stdlib\Glob;
class ConfigProvider
{
public function __invoke()
{
return Factory::fromFiles(Glob::glob(__DIR__ . '/../config/{,*.}config.php', Glob::GLOB_BRACE));
}
}

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\Entity;
namespace Shlinkio\Shlink\Common\Entity;
use Doctrine\ORM\Mapping as ORM;

View File

@@ -0,0 +1,76 @@
<?php
namespace Shlinkio\Shlink\Common\ErrorHandler;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
class ContentBasedErrorHandler implements ErrorHandlerInterface
{
const DEFAULT_CONTENT = 'text/html';
/**
* @var ErrorHandlerManagerInterface
*/
private $errorHandlerManager;
/**
* ContentBasedErrorHandler constructor.
* @param ErrorHandlerManagerInterface|ErrorHandlerManager $errorHandlerManager
*
* @Inject({ErrorHandlerManager::class})
*/
public function __construct(ErrorHandlerManagerInterface $errorHandlerManager)
{
$this->errorHandlerManager = $errorHandlerManager;
}
/**
* Final handler for an application.
*
* @param Request $request
* @param Response $response
* @param null|mixed $err
* @return Response
*/
public function __invoke(Request $request, Response $response, $err = null)
{
// Try to get an error handler for provided request accepted type
$errorHandler = $this->resolveErrorHandlerFromAcceptHeader($request);
return $errorHandler($request, $response, $err);
}
/**
* Tries to resolve
*
* @param Request $request
* @return callable
*/
protected function resolveErrorHandlerFromAcceptHeader(Request $request)
{
// Try to find an error handler for one of the accepted content types
$accepts = $request->hasHeader('Accept') ? $request->getHeaderLine('Accept') : self::DEFAULT_CONTENT;
$accepts = explode(',', $accepts);
foreach ($accepts as $accept) {
if (! $this->errorHandlerManager->has($accept)) {
continue;
}
return $this->errorHandlerManager->get($accept);
}
// If it wasn't possible to find an error handler for accepted content type, use default one if registered
if ($this->errorHandlerManager->has(self::DEFAULT_CONTENT)) {
return $this->errorHandlerManager->get(self::DEFAULT_CONTENT);
}
// It wasn't possible to find an error handler
throw new InvalidArgumentException(sprintf(
'It wasn\'t possible to find an error handler for ["%s"] content types. '
. 'Make sure you have registered at least the default "%s" content type',
implode('", "', $accepts),
self::DEFAULT_CONTENT
));
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Shlinkio\Shlink\Common\ErrorHandler;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
interface ErrorHandlerInterface
{
/**
* Final handler for an application.
*
* @param Request $request
* @param Response $response
* @param null|mixed $err
* @return Response
*/
public function __invoke(Request $request, Response $response, $err = null);
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Shlinkio\Shlink\Common\ErrorHandler;
use Zend\ServiceManager\AbstractPluginManager;
use Zend\ServiceManager\Exception\InvalidServiceException;
class ErrorHandlerManager extends AbstractPluginManager implements ErrorHandlerManagerInterface
{
public function validate($instance)
{
if (is_callable($instance)) {
return;
}
throw new InvalidServiceException(sprintf(
'Only callables are valid plugins for "%s". "%s" provided',
__CLASS__,
is_object($instance) ? get_class($instance) : gettype($instance)
));
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Shlinkio\Shlink\Common\ErrorHandler;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class ErrorHandlerManagerFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when
* creating a service.
* @throws ContainerException if any other error occurs
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->get('config')['error_handler'];
$plugins = isset($config['plugins']) ? $config['plugins'] : [];
return new ErrorHandlerManager($container, $plugins);
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Shlinkio\Shlink\Common\ErrorHandler;
use Interop\Container\ContainerInterface;
interface ErrorHandlerManagerInterface extends ContainerInterface
{
}

View File

@@ -0,0 +1,6 @@
<?php
namespace Shlinkio\Shlink\Common\Exception;
interface ExceptionInterface
{
}

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\Exception;
namespace Shlinkio\Shlink\Common\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\Exception;
namespace Shlinkio\Shlink\Common\Exception;
class RuntimeException extends \RuntimeException implements ExceptionInterface
{

View File

@@ -0,0 +1,10 @@
<?php
namespace Shlinkio\Shlink\Common\Exception;
class WrongIpException extends RuntimeException
{
public static function fromIpAddress($ipAddress, \Exception $prev = null)
{
return new self(sprintf('Provided IP "%s" is invalid', $ipAddress), 0, $prev);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Shlinkio\Shlink\Common\Factory;
use Doctrine\Common\Cache\ApcuCache;
use Doctrine\Common\Cache\ArrayCache;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class CacheFactory implements FactoryInterface
{
const VALID_CACHE_ADAPTERS = [
ApcuCache::class,
ArrayCache::class,
];
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when
* creating a service.
* @throws ContainerException if any other error occurs
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
// Try to get the adapter from config
$config = $container->get('config');
if (isset($config['cache'])
&& isset($config['cache']['adapter'])
&& in_array($config['cache']['adapter'], self::VALID_CACHE_ADAPTERS)
) {
return new $config['cache']['adapter']();
}
// If the adapter has not been set in config, create one based on environment
return env('APP_ENV', 'pro') === 'pro' ? new ApcuCache() : new ArrayCache();
}
}

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\Factory;
namespace Shlinkio\Shlink\Common\Factory;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache;
@@ -33,7 +33,7 @@ class EntityManagerFactory implements FactoryInterface
$dbConfig = isset($globalConfig['database']) ? $globalConfig['database'] : [];
return EntityManager::create($dbConfig, Setup::createAnnotationMetadataConfiguration(
['src/Entity'],
['module/Core/src/Entity'],
$isDevMode,
'data/proxies',
$cache,

View File

@@ -1,15 +1,14 @@
<?php
namespace Acelaya\UrlShortener\Factory;
namespace Shlinkio\Shlink\Common\Factory;
use Doctrine\Common\Cache\ApcuCache;
use Doctrine\Common\Cache\ArrayCache;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class CacheFactory implements FactoryInterface
class TranslatorFactory implements FactoryInterface
{
/**
* Create an object
@@ -25,6 +24,7 @@ class CacheFactory implements FactoryInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return getenv('APP_ENV') === 'pro' ? new ApcuCache() : new ArrayCache();
$config = $container->get('config');
return Translator::factory(isset($config['translator']) ? $config['translator'] : []);
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Shlinkio\Shlink\Common\Middleware;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Zend\I18n\Translator\Translator;
use Zend\Stratigility\MiddlewareInterface;
class LocaleMiddleware implements MiddlewareInterface
{
/**
* @var Translator
*/
private $translator;
/**
* LocaleMiddleware constructor.
* @param Translator $translator
*
* @Inject({"translator"})
*/
public function __construct(Translator $translator)
{
$this->translator = $translator;
}
/**
* 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)
{
if (! $request->hasHeader('Accept-Language')) {
return $out($request, $response);
}
$locale = $request->getHeaderLine('Accept-Language');
$this->translator->setLocale($this->normalizeLocale($locale));
return $out($request, $response);
}
/**
* @param string $locale
* @return string
*/
protected function normalizeLocale($locale)
{
$parts = explode('_', $locale);
if (count($parts) > 1) {
return $parts[0];
}
$parts = explode('-', $locale);
if (count($parts) > 1) {
return $parts[0];
}
return $locale;
}
}

View File

@@ -1,7 +1,7 @@
<?php
namespace Acelaya\UrlShortener\Paginator\Adapter;
namespace Shlinkio\Shlink\Common\Paginator\Adapter;
use Acelaya\UrlShortener\Repository\PaginableRepositoryInterface;
use Shlinkio\Shlink\Common\Repository\PaginableRepositoryInterface;
use Zend\Paginator\Adapter\AdapterInterface;
class PaginableRepositoryAdapter implements AdapterInterface

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\Paginator\Util;
namespace Shlinkio\Shlink\Common\Paginator\Util;
use Zend\Paginator\Paginator;
use Zend\Stdlib\ArrayUtils;

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\Repository;
namespace Shlinkio\Shlink\Common\Repository;
interface PaginableRepositoryInterface
{

View File

@@ -0,0 +1,42 @@
<?php
namespace Shlinkio\Shlink\Common\Service;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
class IpLocationResolver implements IpLocationResolverInterface
{
const SERVICE_PATTERN = 'http://freegeoip.net/json/%s';
/**
* @var Client
*/
private $httpClient;
/**
* IpLocationResolver constructor.
* @param Client $httpClient
*
* @Inject({"httpClient"})
*/
public function __construct(Client $httpClient)
{
$this->httpClient = $httpClient;
}
/**
* @param $ipAddress
* @return array
*/
public function resolveIpLocation($ipAddress)
{
try {
$response = $this->httpClient->get(sprintf(self::SERVICE_PATTERN, $ipAddress));
return json_decode($response->getBody(), true);
} catch (GuzzleException $e) {
throw WrongIpException::fromIpAddress($ipAddress, $e);
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Shlinkio\Shlink\Common\Service;
interface IpLocationResolverInterface
{
/**
* @param $ipAddress
* @return array
*/
public function resolveIpLocation($ipAddress);
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Shlinkio\Shlink\Common\Twig\Extension;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Zend\I18n\Translator\TranslatorInterface;
class TranslatorExtension extends \Twig_Extension implements TranslatorInterface
{
/**
* @var TranslatorInterface
*/
private $translator;
/**
* TranslatorExtension constructor.
* @param TranslatorInterface $translator
*
* @Inject({"translator"})
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* Returns the name of the extension.
*
* @return string The extension name
*/
public function getName()
{
return __CLASS__;
}
public function getFunctions()
{
return [
new \Twig_SimpleFunction('translate', [$this, 'translate']),
new \Twig_SimpleFunction('translate_plural', [$this, 'translatePlural']),
];
}
/**
* Translate a message.
*
* @param string $message
* @param string $textDomain
* @param string $locale
* @return string
*/
public function translate($message, $textDomain = 'default', $locale = null)
{
return $this->translator->translate($message, $textDomain, $locale);
}
/**
* Translate a plural message.
*
* @param string $singular
* @param string $plural
* @param int $number
* @param string $textDomain
* @param string|null $locale
* @return string
*/
public function translatePlural(
$singular,
$plural,
$number,
$textDomain = 'default',
$locale = null
) {
$this->translator->translatePlural($singular, $plural, $number, $textDomain, $locale);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Shlinkio\Shlink\Common\Util;
class DateRange
{
/**
* @var \DateTimeInterface
*/
private $startDate;
/**
* @var \DateTimeInterface
*/
private $endDate;
public function __construct(\DateTimeInterface $startDate = null, \DateTimeInterface $endDate = null)
{
$this->startDate = $startDate;
$this->endDate = $endDate;
}
/**
* @return \DateTimeInterface
*/
public function getStartDate()
{
return $this->startDate;
}
/**
* @return \DateTimeInterface
*/
public function getEndDate()
{
return $this->endDate;
}
/**
* @return bool
*/
public function isEmpty()
{
return is_null($this->startDate) && is_null($this->endDate);
}
}

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\Util;
namespace Shlinkio\Shlink\Common\Util;
trait StringUtilsTrait
{

View File

@@ -0,0 +1,31 @@
<?php
namespace ShlinkioTest\Shlink\Common;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\Common\ConfigProvider;
class ConfigProviderTest extends TestCase
{
/**
* @var ConfigProvider
*/
protected $configProvider;
public function setUp()
{
$this->configProvider = new ConfigProvider();
}
/**
* @test
*/
public function configIsReturned()
{
$config = $this->configProvider->__invoke();
$this->assertArrayHasKey('error_handler', $config);
$this->assertArrayHasKey('middleware_pipeline', $config);
$this->assertArrayHasKey('dependencies', $config);
$this->assertArrayHasKey('twig', $config);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace ShlinkioTest\Shlink\Common\ErrorHandler;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\Common\ErrorHandler\ContentBasedErrorHandler;
use Shlinkio\Shlink\Common\ErrorHandler\ErrorHandlerManager;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
use Zend\ServiceManager\ServiceManager;
class ContentBasedErrorHandlerTest extends TestCase
{
/**
* @var ContentBasedErrorHandler
*/
protected $errorHandler;
public function setUp()
{
$this->errorHandler = new ContentBasedErrorHandler(new ErrorHandlerManager(new ServiceManager(), [
'factories' => [
'text/html' => [$this, 'factory'],
'application/json' => [$this, 'factory'],
],
]));
}
public function factory($container, $name)
{
return function () use ($name) {
return $name;
};
}
/**
* @test
*/
public function correctAcceptHeaderValueInvokesErrorHandler()
{
$request = ServerRequestFactory::fromGlobals()->withHeader('Accept', 'foo/bar,application/json');
$result = $this->errorHandler->__invoke($request, new Response());
$this->assertEquals('application/json', $result);
}
/**
* @test
*/
public function defaultContentTypeIsUsedWhenNoAcceptHeaderisPresent()
{
$request = ServerRequestFactory::fromGlobals();
$result = $this->errorHandler->__invoke($request, new Response());
$this->assertEquals('text/html', $result);
}
/**
* @test
*/
public function defaultContentTypeIsUsedWhenAcceptedContentIsNotSupported()
{
$request = ServerRequestFactory::fromGlobals()->withHeader('Accept', 'foo/bar,text/xml');
$result = $this->errorHandler->__invoke($request, new Response());
$this->assertEquals('text/html', $result);
}
/**
* @test
* @expectedException \Shlinkio\Shlink\Common\Exception\InvalidArgumentException
*/
public function ifNoErrorHandlerIsFoundAnExceptionIsThrown()
{
$this->errorHandler = new ContentBasedErrorHandler(new ErrorHandlerManager(new ServiceManager(), []));
$request = ServerRequestFactory::fromGlobals()->withHeader('Accept', 'foo/bar,text/xml');
$result = $this->errorHandler->__invoke($request, new Response());
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace ShlinkioTest\Shlink\Common\ErrorHandler;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\Common\ErrorHandler\ErrorHandlerManager;
use Shlinkio\Shlink\Common\ErrorHandler\ErrorHandlerManagerFactory;
use Zend\ServiceManager\ServiceManager;
class ErrorHandlerManagerFactoryTest extends TestCase
{
/**
* @var ErrorHandlerManagerFactory
*/
protected $factory;
public function setUp()
{
$this->factory = new ErrorHandlerManagerFactory();
}
/**
* @test
*/
public function serviceIsCreated()
{
$instance = $this->factory->__invoke(new ServiceManager(['services' => [
'config' => [
'error_handler' => [
'plugins' => [],
],
],
]]), '');
$this->assertInstanceOf(ErrorHandlerManager::class, $instance);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace ShlinkioTest\Shlink\Common\ErrorHandler;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\Common\ErrorHandler\ErrorHandlerManager;
use Zend\ServiceManager\ServiceManager;
class ErrorHandlerManagerTest extends TestCase
{
/**
* @var ErrorHandlerManager
*/
protected $pluginManager;
public function setUp()
{
$this->pluginManager = new ErrorHandlerManager(new ServiceManager(), [
'services' => [
'foo' => function () {
},
],
'invokables' => [
'invalid' => \stdClass::class,
]
]);
}
/**
* @test
*/
public function callablesAreReturned()
{
$instance = $this->pluginManager->get('foo');
$this->assertInstanceOf(\Closure::class, $instance);
}
/**
* @test
* @expectedException \Zend\ServiceManager\Exception\InvalidServiceException
*/
public function nonCallablesThrowException()
{
$this->pluginManager->get('invalid');
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace ShlinkioTest\Shlink\Common\Factory;
use Doctrine\Common\Cache\ApcuCache;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\FilesystemCache;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\Common\Factory\CacheFactory;
use Zend\ServiceManager\ServiceManager;
class CacheFactoryTest extends TestCase
{
/**
* @var CacheFactory
*/
protected $factory;
public function setUp()
{
$this->factory = new CacheFactory();
}
public static function tearDownAfterClass()
{
putenv('APP_ENV');
}
/**
* @test
*/
public function productionReturnsApcAdapter()
{
putenv('APP_ENV=pro');
$instance = $this->factory->__invoke($this->createSM(), '');
$this->assertInstanceOf(ApcuCache::class, $instance);
}
/**
* @test
*/
public function developmentReturnsArrayAdapter()
{
putenv('APP_ENV=dev');
$instance = $this->factory->__invoke($this->createSM(), '');
$this->assertInstanceOf(ArrayCache::class, $instance);
}
/**
* @test
*/
public function adapterDefinedInConfigIgnoresEnvironment()
{
putenv('APP_ENV=pro');
$instance = $this->factory->__invoke($this->createSM(ArrayCache::class), '');
$this->assertInstanceOf(ArrayCache::class, $instance);
}
/**
* @test
*/
public function invalidAdapterDefinedInConfigFallbacksToEnvironment()
{
putenv('APP_ENV=pro');
$instance = $this->factory->__invoke($this->createSM(FilesystemCache::class), '');
$this->assertInstanceOf(ApcuCache::class, $instance);
}
private function createSM($cacheAdapter = null)
{
return new ServiceManager(['services' => [
'config' => isset($cacheAdapter) ? [
'cache' => ['adapter' => $cacheAdapter],
] : [],
]]);
}
}

View File

@@ -1,9 +1,9 @@
<?php
namespace AcelayaTest\UrlShortener\Factory;
namespace ShlinkioTest\Shlink\Common\Factory;
use Acelaya\UrlShortener\Factory\EntityManagerFactory;
use Doctrine\ORM\EntityManager;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\Common\Factory\EntityManagerFactory;
use Zend\ServiceManager\ServiceManager;
class EntityManagerFactoryTest extends TestCase

View File

@@ -0,0 +1,31 @@
<?php
namespace ShlinkioTest\Shlink\Common\Factory;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\Common\Factory\TranslatorFactory;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\ServiceManager;
class TranslatorFactoryTest extends TestCase
{
/**
* @var TranslatorFactory
*/
protected $factory;
public function setUp()
{
$this->factory = new TranslatorFactory();
}
/**
* @test
*/
public function serviceIsCreated()
{
$instance = $this->factory->__invoke(new ServiceManager(['services' => [
'config' => [],
]]), '');
$this->assertInstanceOf(Translator::class, $instance);
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace ShlinkioTest\Shlink\Common\Middleware;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator;
class LocaleMiddlewareTest extends TestCase
{
/**
* @var LocaleMiddleware
*/
protected $middleware;
/**
* @var Translator
*/
protected $translator;
public function setUp()
{
$this->translator = Translator::factory(['locale' => 'ru']);
$this->middleware = new LocaleMiddleware($this->translator);
}
/**
* @test
*/
public function whenNoHeaderIsPresentLocaleIsNotChanged()
{
$this->assertEquals('ru', $this->translator->getLocale());
$this->middleware->__invoke(ServerRequestFactory::fromGlobals(), new Response(), function ($req, $resp) {
return $resp;
});
$this->assertEquals('ru', $this->translator->getLocale());
}
/**
* @test
*/
public function whenTheHeaderIsPresentLocaleIsChanged()
{
$this->assertEquals('ru', $this->translator->getLocale());
$request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'es');
$this->middleware->__invoke($request, new Response(), function ($req, $resp) {
return $resp;
});
$this->assertEquals('es', $this->translator->getLocale());
}
/**
* @test
*/
public function localeGetsNormalized()
{
$this->assertEquals('ru', $this->translator->getLocale());
$request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'es_ES');
$this->middleware->__invoke($request, new Response(), function ($req, $resp) {
return $resp;
});
$this->assertEquals('es', $this->translator->getLocale());
$request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'en-US');
$this->middleware->__invoke($request, new Response(), function ($req, $resp) {
return $resp;
});
$this->assertEquals('en', $this->translator->getLocale());
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace ShlinkioTest\Shlink\Common\Paginator;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
use Shlinkio\Shlink\Common\Repository\PaginableRepositoryInterface;
class PaginableRepositoryAdapterTest extends TestCase
{
/**
* @var PaginableRepositoryAdapter
*/
protected $adapter;
/**
* @var ObjectProphecy
*/
protected $repo;
public function setUp()
{
$this->repo = $this->prophesize(PaginableRepositoryInterface::class);
$this->adapter = new PaginableRepositoryAdapter($this->repo->reveal(), 'search', 'order');
}
/**
* @test
*/
public function getItemsFallbacksToFindList()
{
$this->repo->findList(10, 5, 'search', 'order')->shouldBeCalledTimes(1);
$this->adapter->getItems(5, 10);
}
/**
* @test
*/
public function countFallbacksToCountList()
{
$this->repo->countList('search')->shouldBeCalledTimes(1);
$this->adapter->count();
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace ShlinkioTest\Shlink\Common\Service;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\Psr7\Response;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
class IpLocationResolverTest extends TestCase
{
/**
* @var IpLocationResolver
*/
protected $ipResolver;
/**
* @var ObjectProphecy
*/
protected $client;
public function setUp()
{
$this->client = $this->prophesize(Client::class);
$this->ipResolver = new IpLocationResolver($this->client->reveal());
}
/**
* @test
*/
public function correctIpReturnsDecodedInfo()
{
$expected = [
'foo' => 'bar',
'baz' => 'foo',
];
$response = new Response();
$response->getBody()->write(json_encode($expected));
$response->getBody()->rewind();
$this->client->get('http://freegeoip.net/json/1.2.3.4')->willReturn($response)
->shouldBeCalledTimes(1);
$this->assertEquals($expected, $this->ipResolver->resolveIpLocation('1.2.3.4'));
}
/**
* @test
* @expectedException \Shlinkio\Shlink\Common\Exception\WrongIpException
*/
public function guzzleExceptionThrowsShlinkException()
{
$this->client->get('http://freegeoip.net/json/1.2.3.4')->willThrow(new TransferException())
->shouldBeCalledTimes(1);
$this->ipResolver->resolveIpLocation('1.2.3.4');
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace ShlinkioTest\Shlink\Common\Twig\Extension;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
use Zend\I18n\Translator\Translator;
class TranslatorExtensionTest extends TestCase
{
/**
* @var TranslatorExtension
*/
protected $extension;
/**
* @var ObjectProphecy
*/
protected $translator;
public function setUp()
{
$this->translator = $this->prophesize(Translator::class);
$this->extension = new TranslatorExtension($this->translator->reveal());
}
/**
* @test
*/
public function extensionNameIsClassName()
{
$this->assertEquals(TranslatorExtension::class, $this->extension->getName());
}
/**
* @test
*/
public function properFunctionsAreReturned()
{
$funcs = $this->extension->getFunctions();
$this->assertCount(2, $funcs);
foreach ($funcs as $func) {
$this->assertInstanceOf(\Twig_SimpleFunction::class, $func);
}
}
/**
* @test
*/
public function translateFallbacksToTranslator()
{
$this->translator->translate('foo', 'default', null)->shouldBeCalledTimes(1);
$this->extension->translate('foo');
$this->translator->translate('bar', 'baz', 'en')->shouldBeCalledTimes(1);
$this->extension->translate('bar', 'baz', 'en');
}
/**
* @test
*/
public function translatePluralFallbacksToTranslator()
{
$this->translator->translatePlural('foo', 'bar', 'baz', 'default', null)->shouldBeCalledTimes(1);
$this->extension->translatePlural('foo', 'bar', 'baz');
$this->translator->translatePlural('foo', 'bar', 'baz', 'another', 'en')->shouldBeCalledTimes(1);
$this->extension->translatePlural('foo', 'bar', 'baz', 'another', 'en');
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace ShlinkioTest\Shlink\Common\Util;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\Common\Util\DateRange;
class DateRangeTest extends TestCase
{
/**
* @test
*/
public function defaultConstructorSetDatesToNull()
{
$range = new DateRange();
$this->assertNull($range->getStartDate());
$this->assertNull($range->getEndDate());
$this->assertTrue($range->isEmpty());
}
/**
* @test
*/
public function providedDatesAreSet()
{
$startDate = new \DateTime();
$endDate = new \DateTime();
$range = new DateRange($startDate, $endDate);
$this->assertSame($startDate, $range->getStartDate());
$this->assertSame($endDate, $range->getEndDate());
$this->assertFalse($range->isEmpty());
}
}

View File

@@ -0,0 +1,21 @@
<?php
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Shlinkio\Shlink\Core\Action\RedirectAction;
use Shlinkio\Shlink\Core\Service;
return [
'dependencies' => [
'factories' => [
// Services
Service\UrlShortener::class => AnnotatedFactory::class,
Service\VisitsTracker::class => AnnotatedFactory::class,
Service\ShortUrlService::class => AnnotatedFactory::class,
Service\VisitService::class => AnnotatedFactory::class,
// Middleware
RedirectAction::class => AnnotatedFactory::class,
],
],
];

View File

@@ -0,0 +1,15 @@
<?php
use Shlinkio\Shlink\Core\Action\RedirectAction;
return [
'routes' => [
[
'name' => 'long-url-redirect',
'path' => '/{shortCode}',
'middleware' => RedirectAction::class,
'allowed_methods' => ['GET'],
],
],
];

View File

@@ -0,0 +1,11 @@
<?php
return [
'templates' => [
'paths' => [
'module/Core/templates',
],
],
];

View File

@@ -0,0 +1,14 @@
<?php
return [
'translator' => [
'translation_file_patterns' => [
[
'type' => 'gettext',
'base_dir' => __DIR__ . '/../lang',
'pattern' => '%s.mo',
],
],
],
];

View File

@@ -0,0 +1,12 @@
<?php
return [
'zend-expressive' => [
'error_handler' => [
'template_404' => 'core/error/404.html.twig',
'template_error' => 'core/error/error.html.twig',
],
],
];

BIN
module/Core/lang/es.mo Normal file

Binary file not shown.

35
module/Core/lang/es.po Normal file
View File

@@ -0,0 +1,35 @@
msgid ""
msgstr ""
"Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2016-07-21 16:50+0200\n"
"PO-Revision-Date: 2016-07-21 16:51+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.7.1\n"
"X-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: translate;translaePlural;translate_plural\n"
"X-Poedit-SearchPath-0: templates\n"
"X-Poedit-SearchPath-1: config\n"
"X-Poedit-SearchPath-2: src\n"
msgid "Make sure you included all the characters, with no extra punctuation."
msgstr "Asegúrate de haber incluído todos los caracteres, sin puntuación extra."
msgid "Oops!"
msgstr "¡Vaya!"
msgid "This short URL doesn't seem to be valid."
msgstr "Esta URL acortada no parece ser válida."
msgid "URL Not Found"
msgstr "URL no encontrada"
#, php-format
msgid "We encountered a %s %s error."
msgstr "Hemos encontrado un error %s %s."

View File

@@ -1,17 +1,17 @@
<?php
namespace Acelaya\UrlShortener\Middleware\Routable;
namespace Shlinkio\Shlink\Core\Action;
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;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\Service\VisitsTracker;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Zend\Diactoros\Response\RedirectResponse;
use Zend\Stratigility\MiddlewareInterface;
class RedirectMiddleware implements MiddlewareInterface
class RedirectAction implements MiddlewareInterface
{
/**
* @var UrlShortenerInterface
@@ -67,13 +67,13 @@ class RedirectMiddleware implements MiddlewareInterface
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 provided shortCode does not belong to a valid long URL, dispatch next middleware, which will trigger
// a not-found error
if (! isset($longUrl)) {
return $out($request, $response);
return $this->notFoundResponse($request, $response, $out);
}
// Track visit to this shortcode
// Track visit to this short code
$this->visitTracker->track($shortCode);
// Return a redirect response to the long URL.
@@ -81,7 +81,18 @@ class RedirectMiddleware implements MiddlewareInterface
return new RedirectResponse($longUrl);
} catch (\Exception $e) {
// In case of error, dispatch 404 error
return $out($request, $response);
return $this->notFoundResponse($request, $response, $out);
}
}
/**
* @param Request $request
* @param Response $response
* @param callable $out
* @return Response
*/
protected function notFoundResponse(Request $request, Response $response, callable $out)
{
return $out($request, $response->withStatus(404), 'Not Found');
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Shlinkio\Shlink\Core;
use Zend\Config\Factory;
use Zend\Stdlib\Glob;
class ConfigProvider
{
public function __invoke()
{
return Factory::fromFiles(Glob::glob(__DIR__ . '/../config/{,*.}config.php', Glob::GLOB_BRACE));
}
}

View File

@@ -1,8 +1,9 @@
<?php
namespace Acelaya\UrlShortener\Entity;
namespace Shlinkio\Shlink\Core\Entity;
use Acelaya\UrlShortener\Util\StringUtilsTrait;
use Doctrine\ORM\Mapping as ORM;
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
/**
* Class RestToken

View File

@@ -1,16 +1,17 @@
<?php
namespace Acelaya\UrlShortener\Entity;
namespace Shlinkio\Shlink\Core\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
/**
* Class ShortUrl
* @author
* @link
*
* @ORM\Entity(repositoryClass="Acelaya\UrlShortener\Repository\ShortUrlRepository")
* @ORM\Entity(repositoryClass="Shlinkio\Shlink\Core\Repository\ShortUrlRepository")
* @ORM\Table(name="short_urls")
*/
class ShortUrl extends AbstractEntity implements \JsonSerializable
@@ -22,7 +23,14 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable
protected $originalUrl;
/**
* @var string
* @ORM\Column(name="short_code", type="string", nullable=false, length=10, unique=true)
* @ORM\Column(
* name="short_code",
* type="string",
* nullable=false,
* length=10,
* unique=true,
* options={"collation": "utf8_bin"}
* )
*/
protected $shortCode;
/**

View File

@@ -1,14 +1,15 @@
<?php
namespace Acelaya\UrlShortener\Entity;
namespace Shlinkio\Shlink\Core\Entity;
use Doctrine\ORM\Mapping as ORM;
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
/**
* Class Visit
* @author
* @link
*
* @ORM\Entity
* @ORM\Entity(repositoryClass="Shlinkio\Shlink\Core\Repository\VisitRepository")
* @ORM\Table(name="visits")
*/
class Visit extends AbstractEntity implements \JsonSerializable
@@ -39,6 +40,12 @@ class Visit extends AbstractEntity implements \JsonSerializable
* @ORM\JoinColumn(name="short_url_id", referencedColumnName="id")
*/
protected $shortUrl;
/**
* @var VisitLocation
* @ORM\ManyToOne(targetEntity=VisitLocation::class, cascade={"persist"})
* @ORM\JoinColumn(name="visit_location_id", referencedColumnName="id", nullable=true)
*/
protected $visitLocation;
public function __construct()
{
@@ -135,6 +142,24 @@ class Visit extends AbstractEntity implements \JsonSerializable
return $this;
}
/**
* @return VisitLocation
*/
public function getVisitLocation()
{
return $this->visitLocation;
}
/**
* @param VisitLocation $visitLocation
* @return $this
*/
public function setVisitLocation($visitLocation)
{
$this->visitLocation = $visitLocation;
return $this;
}
/**
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
@@ -149,6 +174,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
'date' => isset($this->date) ? $this->date->format(\DateTime::ISO8601) : null,
'remoteAddr' => $this->remoteAddr,
'userAgent' => $this->userAgent,
'visitLocation' => $this->visitLocation,
];
}
}

View File

@@ -0,0 +1,240 @@
<?php
namespace Shlinkio\Shlink\Core\Entity;
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
use Doctrine\ORM\Mapping as ORM;
use Zend\Stdlib\ArraySerializableInterface;
/**
* Class VisitLocation
* @author
* @link
*
* @ORM\Entity()
* @ORM\Table(name="visit_locations")
*/
class VisitLocation extends AbstractEntity implements ArraySerializableInterface, \JsonSerializable
{
/**
* @var string
* @ORM\Column(nullable=true)
*/
protected $countryCode;
/**
* @var string
* @ORM\Column(nullable=true)
*/
protected $countryName;
/**
* @var string
* @ORM\Column(nullable=true)
*/
protected $regionName;
/**
* @var string
* @ORM\Column(nullable=true)
*/
protected $cityName;
/**
* @var string
* @ORM\Column(nullable=true)
*/
protected $latitude;
/**
* @var string
* @ORM\Column(nullable=true)
*/
protected $longitude;
/**
* @var string
* @ORM\Column(nullable=true)
*/
protected $timezone;
/**
* @return string
*/
public function getCountryCode()
{
return $this->countryCode;
}
/**
* @param string $countryCode
* @return $this
*/
public function setCountryCode($countryCode)
{
$this->countryCode = $countryCode;
return $this;
}
/**
* @return string
*/
public function getCountryName()
{
return $this->countryName;
}
/**
* @param string $countryName
* @return $this
*/
public function setCountryName($countryName)
{
$this->countryName = $countryName;
return $this;
}
/**
* @return string
*/
public function getRegionName()
{
return $this->regionName;
}
/**
* @param string $regionName
* @return $this
*/
public function setRegionName($regionName)
{
$this->regionName = $regionName;
return $this;
}
/**
* @return string
*/
public function getCityName()
{
return $this->cityName;
}
/**
* @param string $cityName
* @return $this
*/
public function setCityName($cityName)
{
$this->cityName = $cityName;
return $this;
}
/**
* @return string
*/
public function getLatitude()
{
return $this->latitude;
}
/**
* @param string $latitude
* @return $this
*/
public function setLatitude($latitude)
{
$this->latitude = $latitude;
return $this;
}
/**
* @return string
*/
public function getLongitude()
{
return $this->longitude;
}
/**
* @param string $longitude
* @return $this
*/
public function setLongitude($longitude)
{
$this->longitude = $longitude;
return $this;
}
/**
* @return string
*/
public function getTimezone()
{
return $this->timezone;
}
/**
* @param string $timezone
* @return $this
*/
public function setTimezone($timezone)
{
$this->timezone = $timezone;
return $this;
}
/**
* Exchange internal values from provided array
*
* @param array $array
* @return void
*/
public function exchangeArray(array $array)
{
if (array_key_exists('country_code', $array)) {
$this->setCountryCode($array['country_code']);
}
if (array_key_exists('country_name', $array)) {
$this->setCountryName($array['country_name']);
}
if (array_key_exists('region_name', $array)) {
$this->setRegionName($array['region_name']);
}
if (array_key_exists('city', $array)) {
$this->setCityName($array['city']);
}
if (array_key_exists('latitude', $array)) {
$this->setLatitude($array['latitude']);
}
if (array_key_exists('longitude', $array)) {
$this->setLongitude($array['longitude']);
}
if (array_key_exists('time_zone', $array)) {
$this->setTimezone($array['time_zone']);
}
}
/**
* Return an array representation of the object
*
* @return array
*/
public function getArrayCopy()
{
return [
'countryCode' => $this->countryCode,
'countryName' => $this->countryName,
'regionName' => $this->regionName,
'cityName' => $this->cityName,
'latitude' => $this->latitude,
'longitude' => $this->longitude,
'timezone' => $this->timezone,
];
}
/**
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
{
return $this->getArrayCopy();
}
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Acelaya\UrlShortener\Exception;
namespace Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Common\Exception\RuntimeException;
class InvalidShortCodeException extends RuntimeException
{

View File

@@ -1,5 +1,7 @@
<?php
namespace Acelaya\UrlShortener\Exception;
namespace Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Common\Exception\RuntimeException;
class InvalidUrlException extends RuntimeException
{

View File

@@ -1,9 +1,8 @@
<?php
namespace Acelaya\UrlShortener\Repository;
namespace Shlinkio\Shlink\Core\Repository;
use Acelaya\UrlShortener\Entity\ShortUrl;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryInterface
{

View File

@@ -1,7 +1,8 @@
<?php
namespace Acelaya\UrlShortener\Repository;
namespace Shlinkio\Shlink\Core\Repository;
use Doctrine\Common\Persistence\ObjectRepository;
use Shlinkio\Shlink\Common\Repository\PaginableRepositoryInterface;
interface ShortUrlRepositoryInterface extends ObjectRepository, PaginableRepositoryInterface
{

Some files were not shown because too many files have changed in this diff Show More