Compare commits

...

46 Commits

Author SHA1 Message Date
Alejandro Celaya
6ba4d8e947 Merge pull request #299 from acelaya/feature/repository-tests
Improved repository tests
2018-12-02 19:24:19 +01:00
Alejandro Celaya
3faf6e967f Updated changelog adding v1.15 2018-12-02 19:15:58 +01:00
Alejandro Celaya
a7a5667301 Improved repository tests 2018-12-02 19:13:49 +01:00
Alejandro Celaya
d4924897b2 Merge pull request #298 from acelaya/feature/document-swoole
Feature/document swoole
2018-12-02 10:10:00 +01:00
Alejandro Celaya
f2d39ca55a Added missing comma 2018-12-02 10:05:33 +01:00
Alejandro Celaya
743d052f55 Documented how to serve shlink using swoole 2018-12-02 09:56:52 +01:00
Alejandro Celaya
17dbab5ee8 Created config file examples to serve shlink using different approaches 2018-12-02 09:41:25 +01:00
Alejandro Celaya
8cb5e07c7b Merge pull request #297 from acelaya/feature/remove-helpers
Removed non-needed services from expressive-helpers
2018-12-01 21:59:24 +01:00
Alejandro Celaya
e9972783d2 Removed non-needed services from expressive-helpers 2018-12-01 21:53:46 +01:00
Alejandro Celaya
84f6080a38 Merge pull request #296 from acelaya/feature/fix-lowercase
Feature/fix lowercase
2018-12-01 21:47:40 +01:00
Alejandro Celaya
1b5c1e4e52 Updated changelog 2018-12-01 21:40:11 +01:00
Alejandro Celaya
d7e89ebdae Ensured custom slugs are case sensitive 2018-12-01 21:38:29 +01:00
Alejandro Celaya
aa413dab6d Configured improvements introduced in expressive swoole 2.1 2018-11-29 21:14:24 +01:00
Alejandro Celaya
b876870bd8 Encapsulated in VisitsParams how the itemsPerPage param is handled 2018-11-29 08:02:22 +01:00
Alejandro Celaya
05e56cc845 Merge pull request #293 from acelaya/feature/visits-pagination
Feature/visits pagination
2018-11-28 21:00:37 +01:00
Alejandro Celaya
d6c158ce98 Updated changelog 2018-11-28 20:55:07 +01:00
Alejandro Celaya
1d4ef4e9a4 Ensured pagination params in visits list are properly parsed to integer 2018-11-28 20:53:04 +01:00
Alejandro Celaya
4d2684be52 Updated swagger docs for visits including everything related to pagination 2018-11-28 20:46:52 +01:00
Alejandro Celaya
6947805b5c Updated to zend-expressive-swoole 2.0.1 removing all workarounds 2018-11-28 20:43:44 +01:00
Alejandro Celaya
d0e0aea0f1 Updated visits to support pagination 2018-11-28 20:39:08 +01:00
Alejandro Celaya
b0f250ed8a Created factory method to build VisitParams from a raw dataset 2018-11-28 19:58:45 +01:00
Alejandro Celaya
45254606d4 Added DTO used to pass filtering params to VisitsTracker 2018-11-27 21:09:27 +01:00
Alejandro Celaya
03ee46d903 Merge pull request #290 from acelaya/feature/coding-standard
Updated project to use external coding standard
2018-11-26 20:53:51 +01:00
Alejandro Celaya
c4afc7a923 Updated project to use external coding standard 2018-11-26 20:46:43 +01:00
Alejandro Celaya
afa2a5b0f0 Merge pull request #284 from acelaya/feature/swoole
Feature/swoole
2018-11-25 22:12:50 +01:00
Alejandro Celaya
b40057d423 Fixed typo in changelog 2018-11-25 21:33:25 +01:00
Alejandro Celaya
282ffef200 Ensured different loggers are used for swoole and for the app regular logs 2018-11-25 17:14:03 +01:00
Alejandro Celaya
22b02de405 Updated swoole docker image so that it retries the start command until status code is 0 2018-11-25 12:44:49 +01:00
Alejandro Celaya
f0330e9ae3 Ensured CloseDbConnectionMiddleware clears the entity manager 2018-11-24 13:24:43 +01:00
Alejandro Celaya
0c26490e3f Added info about swoole in changelog 2018-11-24 13:18:50 +01:00
Alejandro Celaya
cfaecd93e4 Added swoole extension to travis 2018-11-24 13:11:26 +01:00
Alejandro Celaya
ccbc6c7a75 Created middleware which closes DB connection after every request 2018-11-24 12:55:00 +01:00
Alejandro Celaya
2fc2ad98aa Updated config so that shlink logger dynamically uses standard output when running with swoole 2018-11-24 09:38:00 +01:00
Alejandro Celaya
16590b2dbb Prepared project to support both swoole and regular app servers with fast cgi 2018-11-24 08:43:48 +01:00
Alejandro Celaya
f40349479e Used more strict types in UrlShortener private methods 2018-11-24 07:52:57 +01:00
Alejandro Celaya
9f60c8dffe Merge pull request #280 from acelaya/feature/oneliner-type
Feature/oneliner type
2018-11-20 19:42:23 +01:00
Alejandro Celaya
5abd9d1a40 Made test properties to be private instead of protected 2018-11-20 19:37:22 +01:00
Alejandro Celaya
0ae5a53d86 Enforced property types comments in one line 2018-11-20 19:30:27 +01:00
Alejandro Celaya
15a70d0157 Merge pull request #278 from acelaya/feature/del-translations
Feature/del translations
2018-11-18 20:30:53 +01:00
Alejandro Celaya
ededb68ef1 Added changelog for unreleased changes 2018-11-18 20:20:30 +01:00
Alejandro Celaya
09add5fbff Moved locale middleware to before the not found handler, so that it never gets executed otherwise 2018-11-18 20:15:37 +01:00
Alejandro Celaya
e30f49a791 Simplified error templates 2018-11-18 20:04:12 +01:00
Alejandro Celaya
64737b741b Removed CLI language param from installation 2018-11-18 19:55:23 +01:00
Alejandro Celaya
d4d65bdf37 Added missing X-Api-Key header to cross domain middleware 2018-11-18 17:03:50 +01:00
Alejandro Celaya
90732a4fad Removed translations from Rest module 2018-11-18 16:28:04 +01:00
Alejandro Celaya
c5015f5828 Removed translations from CLI module 2018-11-18 16:02:52 +01:00
222 changed files with 1928 additions and 2584 deletions

View File

@@ -18,6 +18,7 @@ matrix:
before_install:
- echo 'extension = memcached.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
- yes | pecl install swoole
- phpenv config-rm xdebug.ini || return 0
install:

View File

@@ -4,6 +4,40 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
## 1.15.0 - 2018-12-02
#### Added
* [#208](https://github.com/shlinkio/shlink/issues/208) Added initial support to run shlink using [swoole](https://www.swoole.co.uk/), a non-blocking IO server which improves the performance of shlink from 4 to 10 times.
Run shlink with `./vendor/bin/zend-expressive-swoole start` to start-up the service, which will be exposed in port `8080`.
Adding the `-d` flag, it will be started as a background service. Then you can use the `./vendor/bin/zend-expressive-swoole stop` command in order to stop it.
* [#266](https://github.com/shlinkio/shlink/issues/266) Added pagination to `GET /short-urls/{shortCode}/visits` endpoint.
In order to make it backwards compatible, it keeps returning all visits by default, but it now allows to provide the `page` and `itemsPerPage` query parameters in order to configure the number of items to get.
#### Changed
* [#267](https://github.com/shlinkio/shlink/issues/267) API responses and the CLI interface is no longer translated and uses english always. Only not found error templates are still translated.
* [#289](https://github.com/shlinkio/shlink/issues/289) Extracted coding standard rules to a separated package.
* [#273](https://github.com/shlinkio/shlink/issues/273) Improved code coverage in repository classes.
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#278](https://github.com/shlinkio/shlink/pull/278) Added missing `X-Api-Key` header to the list of valid cross domain headers.
* [#295](https://github.com/shlinkio/shlink/pull/295) Fixed custom slugs so that they are case sensitive and do not try to lowercase provided values.
## 1.14.1 - 2018-11-17
#### Added

152
README.md
View File

@@ -44,52 +44,132 @@ Despite how you built the project, you are going to need to install it now, by f
* If you are going to use MySQL or PostgreSQL, create an empty database with the name of your choice.
* Recursively grant write permissions to the `data` directory. Shlink uses it to cache some information.
* Setup the application by running the `bin/install` script. It is a command line tool that will guide you through the installation process. **Take into account that this tool has to be run directly on the server where you plan to host Shlink. Do not run it before uploading/moving it there.**
* Configure the web server of your choice to serve shlink using your short domain.
* Expose shlink to the web, either by using a traditional web server + fast CGI approach, or by using a [swoole](https://www.swoole.co.uk/) non-blocking server.
For example, assuming your domain is doma.in and shlink is in the `/path/to/shlink` folder, this would be the basic configuration for Nginx and Apache.
* **Using a web server:**
*Nginx:*
For example, assuming your domain is doma.in and shlink is in the `/path/to/shlink` folder, these would be the basic configurations for Nginx and Apache.
```nginx
server {
server_name doma.in;
listen 80;
root /path/to/shlink/public;
index index.php;
charset utf-8;
*Nginx:*
location / {
try_files $uri $uri/ /index.php$is_args$args;
```nginx
server {
server_name doma.in;
listen 80;
root /path/to/shlink/public;
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
fastcgi_index index.php;
include fastcgi.conf;
}
location ~ /\.ht {
deny all;
}
}
```
*Apache:*
```apache
<VirtualHost *:80>
ServerName doma.in
DocumentRoot "/path/to/shlink/public"
<Directory "/path/to/shlink/public">
Options FollowSymLinks Includes ExecCGI
AllowOverride all
Order allow,deny
Allow from all
</Directory>
</VirtualHost>
```
* **Using swoole:**
**Important!** Swoole support is still experimental. Use it with care, and report any found issue.
First you need to install the swoole PHP extension with [pecl](https://pecl.php.net/package/swoole), `pecl install swoole`.
Once installed, it's actually pretty easy to get shlink up and running with swoole. Just run `./vendor/bin/zend-expressive-swoole start -d` and you will get shlink running on port 8080.
However, by doing it this way, you are loosing all the access logs, and the service won't be automatically run if the server has to be restarted.
For that reason, you should create a daemon script, in `/etc/init.d/shlink_swoole`, like this one, replacing `/path/to/shlink` by the path to your shlink installation:
```bash
#!/bin/bash
### BEGIN INIT INFO
# Provides: shlink_swoole
# Required-Start: $local_fs $network $named $time $syslog
# Required-Stop: $local_fs $network $named $time $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: Shlink non-blocking server with swoole
### END INIT INFO
SCRIPT=/path/to/shlink/vendor/bin/zend-expressive-swoole\ start
RUNAS=root
PIDFILE=/var/run/shlink_swoole.pid
LOGDIR=/var/log/shlink
LOGFILE=${LOGDIR}/shlink_swoole.log
start() {
if [[ -f "$PIDFILE" ]] && kill -0 $(cat "$PIDFILE"); then
echo 'Shlink with swoole already running' >&2
return 1
fi
echo 'Starting shlink with swoole' >&2
mkdir -p "$LOGDIR"
touch "$LOGFILE"
local CMD="$SCRIPT &> \"$LOGFILE\" & echo \$!"
su -c "$CMD" $RUNAS > "$PIDFILE"
echo 'Shlink started' >&2
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
fastcgi_index index.php;
include fastcgi.conf;
stop() {
if [[ ! -f "$PIDFILE" ]] || ! kill -0 $(cat "$PIDFILE"); then
echo 'Shlink with swoole not running' >&2
return 1
fi
echo 'Stopping shlink with swoole' >&2
kill -15 $(cat "$PIDFILE") && rm -f "$PIDFILE"
echo 'Shlink stopped' >&2
}
location ~ /\.ht {
deny all;
}
}
```
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
*)
echo "Usage: $0 {start|stop|restart}"
esac
```
*Apache:*
Then run these commands to enable the service and start it:
```apache
<VirtualHost *:80>
ServerName doma.in
DocumentRoot "/path/to/shlink/public"
* `sudo chmod +x /etc/init.d/shlink_swoole`
* `sudo update-rc.d shlink_swoole defaults`
* `sudo update-rc.d shlink_swoole enable`
* `/etc/init.d/shlink_swoole start`
<Directory "/path/to/shlink/public">
Options FollowSymLinks Includes ExecCGI
AllowOverride all
Order allow,deny
Allow from all
</Directory>
</VirtualHost>
```
Now again, you can access shlink on port 8080, but this time the service will be automatically run at system start-up, and all access logs will be written in `/var/log/shlink/shlink_swoole.log` (you will probably want to [rotate those logs](https://www.digitalocean.com/community/tutorials/how-to-manage-logfiles-with-logrotate-on-ubuntu-16-04). You can find an example logrotate config file [here](data/infra/examples/shlink-daemon-logrotate.conf)).
* Generate your first API key by running `bin/cli api-key:generate`. You will need the key in order to interact with shlink's API.
* Finally access to [https://app.shlink.io](https://app.shlink.io) and configure your server to start creating short URLs.
@@ -116,6 +196,8 @@ Those tasks can be performed using shlink's CLI, so it should be easy to schedul
*Any of those commands accept the `-q` flag, which makes it not display any output. This is recommended when configuring the commands as cron jobs.*
In future versions, it is planed that, when using **swoole** to serve shlink, some of these tasks are automatically run without blocking the request and also, without having to configure cron jobs. Probably resolving IP locations and generating previews.
## Update to new version
When a new Shlink version is available, you don't need to repeat the entire process yourself. Instead, follow these steps:

View File

@@ -3,8 +3,11 @@
declare(strict_types=1);
use Interop\Container\ContainerInterface;
use Shlinkio\Shlink\Common\Exec\ExecutionContext;
use Symfony\Component\Console\Application as CliApp;
/** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php';
putenv(sprintf('CURRENT_SHLINK_CONTEXT=%s', ExecutionContext::CLI));
$container->get(CliApp::class)->run();

View File

@@ -42,6 +42,7 @@
"zendframework/zend-expressive-fastroute": "^3.0",
"zendframework/zend-expressive-helpers": "^5.0",
"zendframework/zend-expressive-platesrenderer": "^2.0",
"zendframework/zend-expressive-swoole": "^2.1",
"zendframework/zend-i18n": "^2.7",
"zendframework/zend-inputfilter": "^2.8",
"zendframework/zend-paginator": "^2.6",
@@ -55,8 +56,7 @@
"phpstan/phpstan": "^0.10.0",
"phpunit/phpcov": "^5.0",
"phpunit/phpunit": "^7.3",
"slevomat/coding-standard": "^4.0",
"squizlabs/php_codesniffer": "^3.2.3",
"shlinkio/php-coding-standard": "~1.0.0",
"symfony/dotenv": "^4.0",
"symfony/var-dumper": "^4.0",
"zendframework/zend-component-installer": "^2.1",
@@ -119,7 +119,7 @@
"test:pretty": [
"@test",
"phpcov merge build --html build/html"
"phpdbg -qrr vendor/bin/phpcov merge build --html build/html"
],
"test:unit:pretty": "phpdbg -qrr vendor/bin/phpunit --coverage-html build/coverage --order-by=random",

View File

@@ -4,18 +4,13 @@ declare(strict_types=1);
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory;
use Zend\Expressive;
use Zend\Expressive\Container;
use Zend\Expressive\Helper;
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
use Zend\ServiceManager\Factory\InvokableFactory;
return [
'dependencies' => [
'factories' => [
ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
Helper\ServerUrlHelper::class => InvokableFactory::class,
],
'delegators' => [

View File

@@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Common;
use function Shlinkio\Shlink\Common\env;
return [
@@ -10,9 +10,9 @@ return [
'proxies_dir' => 'data/proxies',
],
'connection' => [
'user' => Common\env('DB_USER'),
'password' => Common\env('DB_PASSWORD'),
'dbname' => Common\env('DB_NAME', 'shlink'),
'user' => env('DB_USER'),
'password' => env('DB_PASSWORD'),
'dbname' => env('DB_NAME', 'shlink'),
'charset' => 'utf8',
],
],

View File

@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
use Zend\ConfigAggregator\ConfigAggregator;
return [
'debug' => true,
'config_cache_enabled' => false,
ConfigAggregator::ENABLE_CACHE => false,
];

View File

@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Monolog\Processor;
use Zend\Expressive\Swoole\Log\AccessLogInterface;
use const PHP_EOL;
return [
@@ -19,13 +21,19 @@ return [
],
'handlers' => [
'rotating_file_handler' => [
'shlink_rotating_handler' => [
'class' => RotatingFileHandler::class,
'level' => Logger::INFO,
'filename' => 'data/log/shlink_log.log',
'max_files' => 30,
'formatter' => 'dashed',
],
'swoole_access_handler' => [
'class' => StreamHandler::class,
'level' => Logger::INFO,
'stream' => 'php://stdout',
'formatter' => 'dashed',
],
],
'processors' => [
@@ -39,9 +47,30 @@ return [
'loggers' => [
'Shlink' => [
'handlers' => ['rotating_file_handler'],
'handlers' => ['shlink_rotating_handler'],
'processors' => ['exception_with_new_line', 'psr3'],
],
'Swoole' => [
'handlers' => ['swoole_access_handler'],
'processors' => ['psr3'],
],
],
],
'dependencies' => [
'factories' => [
'Logger_Shlink' => Common\Factory\LoggerFactory::class,
'Logger_Swoole' => Common\Factory\LoggerFactory::class,
AccessLogInterface::class => Common\Logger\Swoole\AccessLogFactory::class,
],
],
'zend-expressive-swoole' => [
'swoole-http-server' => [
'logger' => [
'logger_name' => 'Logger_Swoole',
],
],
],

View File

@@ -1,11 +1,13 @@
<?php
declare(strict_types=1);
use Monolog\Logger;
return [
'logger' => [
'handlers' => [
'rotating_file_handler' => [
'shlink_rotating_handler' => [
'level' => Logger::DEBUG,
],
],

View File

@@ -10,11 +10,18 @@ return [
'middleware_pipeline' => [
'pre-routing' => [
'middleware' => [
ErrorHandler::class,
Expressive\Helper\ContentLengthMiddleware::class,
Common\Middleware\LocaleMiddleware::class,
],
'middleware' => (function () {
$middleware = [
ErrorHandler::class,
Expressive\Helper\ContentLengthMiddleware::class,
];
if (Common\Exec\ExecutionContext::currentContextIsSwoole()) {
$middleware[] = Common\Middleware\CloseDbConnectionMiddleware::class;
}
return $middleware;
})(),
'priority' => 12,
],
'pre-routing-rest' => [
@@ -47,6 +54,9 @@ return [
'post-routing' => [
'middleware' => [
Expressive\Router\Middleware\DispatchMiddleware::class,
// Only if a not found error is triggered, set-up the locale to be used
Common\Middleware\LocaleMiddleware::class,
Core\Response\NotFoundHandler::class,
],
'priority' => 1,

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
use Cocur\Slugify\Slugify;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
return [
'slugify_options' => [
'lowercase' => false,
],
'dependencies' => [
'factories' => [
Slugify::class => ConfigAbstractFactory::class,
],
],
ConfigAbstractFactory::class => [
Slugify::class => ['config.slugify_options'],
],
];

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
return [
'zend-expressive-swoole' => [
'enable_coroutine' => true,
'swoole-http-server' => [
'host' => '0.0.0.0',
'process-name' => 'shlink',
'static-files' => [
'enable' => false,
],
],
],
];

View File

@@ -1,9 +1,11 @@
<?php
declare(strict_types=1);
use Zend\ConfigAggregator\ConfigAggregator;
return [
'debug' => false,
'config_cache_enabled' => true,
ConfigAggregator::ENABLE_CACHE => true,
];

View File

@@ -6,17 +6,13 @@ namespace Shlinkio\Shlink;
use Acelaya\ExpressiveErrorHandler;
use Zend\ConfigAggregator;
use Zend\Expressive;
use function class_exists;
return (new ConfigAggregator\ConfigAggregator([
Expressive\ConfigProvider::class,
Expressive\Router\ConfigProvider::class,
Expressive\Router\FastRouteRouter\ConfigProvider::class,
Expressive\Plates\ConfigProvider::class,
Expressive\Helper\ConfigProvider::class,
class_exists(Expressive\Swoole\ConfigProvider::class)
? Expressive\Swoole\ConfigProvider::class
: new ConfigAggregator\ArrayProvider([]),
Expressive\Swoole\ConfigProvider::class,
ExpressiveErrorHandler\ConfigProvider::class,
Common\ConfigProvider::class,
Core\ConfigProvider::class,

View File

@@ -0,0 +1,11 @@
<VirtualHost *:80>
ServerName doma.in
DocumentRoot "/path/to/shlink/public"
<Directory "/path/to/shlink/public">
Options FollowSymLinks Includes ExecCGI
AllowOverride all
Order allow,deny
Allow from all
</Directory>
</VirtualHost>

View File

@@ -0,0 +1,22 @@
server {
server_name doma.in;
listen 80;
root /path/to/shlink/public;
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
fastcgi_index index.php;
include fastcgi.conf;
}
location ~ /\.ht {
deny all;
}
}

View File

@@ -0,0 +1,13 @@
/var/log/shlink/shlink_swoole.log {
su root root
daily
missingok
rotate 120
compress
delaycompress
notifempty
create 0640 root root
postrotate
/etc/init.d/shlink_swoole restart
endscript
}

View File

@@ -0,0 +1,54 @@
#!/bin/bash
### BEGIN INIT INFO
# Provides: shlink_swoole
# Required-Start: $local_fs $network $named $time $syslog
# Required-Stop: $local_fs $network $named $time $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: Shlink non-blocking server with swoole
### END INIT INFO
SCRIPT=/path/to/shlink/vendor/bin/zend-expressive-swoole\ start
RUNAS=root
PIDFILE=/var/run/shlink_swoole.pid
LOGDIR=/var/log/shlink
LOGFILE=${LOGDIR}/shlink_swoole.log
start() {
if [[ -f "$PIDFILE" ]] && kill -0 $(cat "$PIDFILE"); then
echo 'Shlink with swoole already running' >&2
return 1
fi
echo 'Starting shlink with swoole' >&2
mkdir -p "$LOGDIR"
touch "$LOGFILE"
local CMD="$SCRIPT &> \"$LOGFILE\" & echo \$!"
su -c "$CMD" $RUNAS > "$PIDFILE"
echo 'Shlink started' >&2
}
stop() {
if [[ ! -f "$PIDFILE" ]] || ! kill -0 $(cat "$PIDFILE"); then
echo 'Shlink with swoole not running' >&2
return 1
fi
echo 'Stopping shlink with swoole' >&2
kill -15 $(cat "$PIDFILE") && rm -f "$PIDFILE"
echo 'Shlink stopped' >&2
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
*)
echo "Usage: $0 {start|stop|restart}"
esac

View File

@@ -0,0 +1,98 @@
FROM php:7.1.22-cli-alpine3.7
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
RUN apk update
# Install common php extensions
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-install iconv
RUN docker-php-ext-install mbstring
RUN docker-php-ext-install calendar
RUN apk add --no-cache --virtual sqlite-libs
RUN apk add --no-cache --virtual sqlite-dev
RUN docker-php-ext-install pdo_sqlite
RUN apk add --no-cache --virtual icu-dev
RUN docker-php-ext-install intl
RUN apk add --no-cache --virtual zlib-dev
RUN docker-php-ext-install zip
RUN apk add --no-cache --virtual libmcrypt-dev
RUN docker-php-ext-install mcrypt
RUN apk add --no-cache --virtual libpng-dev
RUN docker-php-ext-install gd
# Install redis extension
ADD https://github.com/phpredis/phpredis/archive/3.1.4.tar.gz /tmp/phpredis.tar.gz
RUN mkdir -p /usr/src/php/ext/redis\
&& tar xf /tmp/phpredis.tar.gz -C /usr/src/php/ext/redis --strip-components=1
# configure and install
RUN docker-php-ext-configure redis\
&& docker-php-ext-install redis
# cleanup
RUN rm /tmp/phpredis.tar.gz
# Install memcached extension
RUN apk add --no-cache --virtual cyrus-sasl-dev
RUN apk add --no-cache --virtual libmemcached-dev
ADD https://github.com/php-memcached-dev/php-memcached/archive/php7.tar.gz /tmp/memcached.tar.gz
RUN mkdir -p /usr/src/php/ext/memcached\
&& tar xf /tmp/memcached.tar.gz -C /usr/src/php/ext/memcached --strip-components=1
# configure and install
RUN docker-php-ext-configure memcached\
&& docker-php-ext-install memcached
# cleanup
RUN rm /tmp/memcached.tar.gz
# Install APCu extension
ADD https://pecl.php.net/get/apcu-5.1.3.tgz /tmp/apcu.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu\
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1
# configure and install
RUN docker-php-ext-configure apcu\
&& docker-php-ext-install apcu
# cleanup
RUN rm /tmp/apcu.tar.gz
# Install APCu-BC extension
ADD https://pecl.php.net/get/apcu_bc-1.0.3.tgz /tmp/apcu_bc.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu-bc\
&& tar xf /tmp/apcu_bc.tar.gz -C /usr/src/php/ext/apcu-bc --strip-components=1
# configure and install
RUN docker-php-ext-configure apcu-bc\
&& docker-php-ext-install apcu-bc
# cleanup
RUN rm /tmp/apcu_bc.tar.gz
# Load APCU.ini before APC.ini
RUN rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini
RUN echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
# Install swoole
# First line fixes an error when installing pecl extensions. Found in https://github.com/docker-library/php/issues/233
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS && \
pecl install swoole && \
docker-php-ext-enable swoole && \
apk del .phpize-deps
# Install composer
RUN php -r "readfile('https://getcomposer.org/installer');" | php
RUN chmod +x composer.phar
RUN mv composer.phar /usr/local/bin/composer
# Make home directory writable by anyone
RUN chmod 777 /home
VOLUME /home/shlink
WORKDIR /home/shlink
# Expose swoole port
EXPOSE 8080
CMD /usr/local/bin/composer update && \
# When restarting the container, swoole might think it is already in execution
# This forces the app to be started every second until the exit code is 0
until php ./vendor/bin/zend-expressive-swoole start; do sleep 1 ; done

View File

@@ -6,3 +6,9 @@ services:
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
shlink_swoole:
user: 1000:1000
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro

View File

@@ -26,6 +26,18 @@ services:
links:
- shlink_db
shlink_swoole:
container_name: shlink_swoole
build:
context: .
dockerfile: ./data/infra/swoole.Dockerfile
ports:
- "8080:8080"
volumes:
- ./:/home/shlink
links:
- shlink_db
shlink_db:
container_name: shlink_db
build:

View File

@@ -33,6 +33,24 @@
"schema": {
"type": "string"
}
},
{
"name": "page",
"in": "query",
"description": "The page to display. Defaults to 1",
"required": false,
"schema": {
"type": "number"
}
},
{
"name": "itemsPerPage",
"in": "query",
"description": "The amount of items to return on every page. Defaults to all the items",
"required": false,
"schema": {
"type": "number"
}
}
],
"security": [
@@ -59,6 +77,9 @@
"items": {
"$ref": "../definitions/Visit.json"
}
},
"pagination": {
"$ref": "../definitions/Pagination.json"
}
}
}
@@ -96,7 +117,14 @@
"userAgent": "some_web_crawler/1.4",
"visitLocation": null
}
]
],
"pagination": {
"currentPage": 5,
"pagesCount": 12,
"itemsPerPage": 10,
"itemsInCurrentPage": 10,
"totalItems": 115
}
}
}
}

View File

@@ -1,2 +1,9 @@
#!/usr/bin/env bash
docker exec -it shlink_php /bin/sh -c "cd /home/shlink/www && $*"
# Run docker containers if they are not up yet
if [[ $(docker ps | grep shlink_swoole) ]]; then :
else
docker-compose up -d
fi
docker exec -it shlink_swoole /bin/sh -c "$*"

View File

@@ -3,12 +3,9 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI;
use function Shlinkio\Shlink\Common\env;
return [
'cli' => [
'locale' => env('CLI_LOCALE', 'en'),
'commands' => [
Command\ShortUrl\GenerateShortUrlCommand::NAME => Command\ShortUrl\GenerateShortUrlCommand::class,
Command\ShortUrl\ResolveUrlCommand::NAME => Command\ShortUrl\ResolveUrlCommand::class,

View File

@@ -10,8 +10,8 @@ use Shlinkio\Shlink\Core\Service;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Symfony\Component\Console\Application;
use Symfony\Component\Lock;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory;
return [
@@ -29,8 +29,8 @@ return [
Command\Visit\ProcessVisitsCommand::class => ConfigAbstractFactory::class,
Command\Visit\UpdateDbCommand::class => ConfigAbstractFactory::class,
Command\Config\GenerateCharsetCommand::class => ConfigAbstractFactory::class,
Command\Config\GenerateSecretCommand::class => ConfigAbstractFactory::class,
Command\Config\GenerateCharsetCommand::class => InvokableFactory::class,
Command\Config\GenerateSecretCommand::class => InvokableFactory::class,
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
@@ -44,47 +44,28 @@ return [
],
ConfigAbstractFactory::class => [
Command\ShortUrl\GenerateShortUrlCommand::class => [
Service\UrlShortener::class,
'translator',
'config.url_shortener.domain',
],
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class, 'translator'],
Command\ShortUrl\ListShortUrlsCommand::class => [
Service\ShortUrlService::class,
'translator',
'config.url_shortener.domain',
],
Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class, 'translator'],
Command\ShortUrl\GeneratePreviewCommand::class => [
Service\ShortUrlService::class,
PreviewGenerator::class,
'translator',
],
Command\ShortUrl\DeleteShortUrlCommand::class => [
Service\ShortUrl\DeleteShortUrlService::class,
'translator',
],
Command\ShortUrl\GenerateShortUrlCommand::class => [Service\UrlShortener::class, 'config.url_shortener.domain'],
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class],
Command\ShortUrl\ListShortUrlsCommand::class => [Service\ShortUrlService::class, 'config.url_shortener.domain'],
Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class],
Command\ShortUrl\GeneratePreviewCommand::class => [Service\ShortUrlService::class, PreviewGenerator::class],
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
Command\Visit\ProcessVisitsCommand::class => [
Service\VisitService::class,
IpLocationResolverInterface::class,
Lock\Factory::class,
'translator',
],
Command\Visit\UpdateDbCommand::class => [DbUpdater::class, 'translator'],
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
Command\Config\GenerateCharsetCommand::class => ['translator'],
Command\Config\GenerateSecretCommand::class => ['translator'],
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class],
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
Command\Api\ListKeysCommand::class => [ApiKeyService::class],
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, 'translator'],
Command\Api\DisableKeyCommand::class => [ApiKeyService::class, 'translator'],
Command\Api\ListKeysCommand::class => [ApiKeyService::class, 'translator'],
Command\Tag\ListTagsCommand::class => [Service\Tag\TagService::class, Translator::class],
Command\Tag\CreateTagCommand::class => [Service\Tag\TagService::class, Translator::class],
Command\Tag\RenameTagCommand::class => [Service\Tag\TagService::class, Translator::class],
Command\Tag\DeleteTagsCommand::class => [Service\Tag\TagService::class, Translator::class],
Command\Tag\ListTagsCommand::class => [Service\Tag\TagService::class],
Command\Tag\CreateTagCommand::class => [Service\Tag\TagService::class],
Command\Tag\RenameTagCommand::class => [Service\Tag\TagService::class],
Command\Tag\DeleteTagsCommand::class => [Service\Tag\TagService::class],
],
];

View File

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

Binary file not shown.

View File

@@ -1,403 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2018-11-17 14:29+0100\n"
"PO-Revision-Date: 2018-11-17 14:29+0100\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 2.0.6\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"
msgid "Disables an API key."
msgstr "Desahbilita una clave de API."
msgid "The API key to disable"
msgstr "La clave de API a deshabilitar"
#, php-format
msgid "API key \"%s\" properly disabled"
msgstr "Clave de API \"%s\" deshabilitada correctamente"
#, php-format
msgid "API key \"%s\" does not exist."
msgstr "La clave de API \"%s\" no existe."
msgid "Generates a new valid API key."
msgstr "Genera una nueva clave de API válida."
msgid "The date in which the API key should expire. Use any valid PHP format."
msgstr ""
"La fecha en la que la clave de API debe expirar. Utiliza cualquier valor "
"válido en PHP."
#, php-format
msgid "Generated API key: \"%s\""
msgstr "Generada clave de API. \"%s\""
msgid "Lists all the available API keys."
msgstr "Lista todas las claves de API disponibles."
msgid "Tells if only enabled API keys should be returned."
msgstr "Define si sólo las claves de API habilitadas deben ser devueltas."
msgid "Key"
msgstr "Clave"
msgid "Is enabled"
msgstr "Está habilitada"
msgid "Expiration date"
msgstr "Fecha de caducidad"
#, 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"
#, php-format
msgid "Character set: \"%s\""
msgstr "Grupo de caracteres: \"%s\""
msgid ""
"Generates a random secret string that can be used for JWT token encryption"
msgstr ""
"Genera una cadena de caracteres aleatoria que puede ser usada para cifrar "
"tokens JWT"
#, php-format
msgid "Secret key: \"%s\""
msgstr "Clave secreta: \"%s\""
msgid "Deletes a short URL"
msgstr "Elimina una URL"
msgid "The short code for the short URL to be deleted"
msgstr "El código corto de la URL corta a eliminar"
msgid ""
"Ignores the safety visits threshold check, which could make short URLs with "
"many visits to be accidentally deleted"
msgstr ""
"Ignora el límite de seguridad de visitas, pudiendo resultar en el borrado "
"accidental de URLs con muchas visitas"
#, php-format
msgid "Provided short code \"%s\" could not be found."
msgstr "El código corto proporcionado \"%s\" no ha podido ser encontrado."
#, php-format
msgid ""
"It was not possible to delete the short URL with short code \"%s\" because "
"it has more than %s visits."
msgstr ""
"No se pudo eliminar la URL acortada con código corto \"%s\" porque tiene más "
"de %s visitas."
msgid "Do you want to delete it anyway?"
msgstr "¿Aún así quieres eliminarla?"
msgid "Short URL was not deleted."
msgstr "La URL corta no ha sido eliminada."
#, php-format
msgid "Short URL with short code \"%s\" successfully deleted."
msgstr "La URL acortada con el código corto \"%s\" eliminada correctamente."
msgid ""
"Processes and generates the previews for every URL, improving performance "
"for later web requests."
msgstr ""
"Procesa y genera las vistas previas para cada URL, mejorando el rendimiento "
"para peticiones web posteriores."
msgid "Finished processing all URLs"
msgstr "Finalizado el procesado de todas las URLs"
#, php-format
msgid "Processing URL %s..."
msgstr "Procesando URL %s..."
msgid " <info>Success!</info>"
msgstr " <info>¡Correcto!</info>"
msgid "Error"
msgstr "Error"
msgid "Generates a short URL for provided long URL and returns it"
msgstr "Genera una URL corta para la URL larga proporcionada y la devuelve"
msgid "The long URL to parse"
msgstr "La URL larga a procesar"
msgid "Tags to apply to the new short URL"
msgstr "Etiquetas a aplicar a la nueva URL acortada"
msgid ""
"The date from which this short URL will be valid. If someone tries to access "
"it before this date, it will not be found."
msgstr ""
"La fecha desde la cual será válida esta URL acortada. Si alguien intenta "
"acceder a ella antes de esta fecha, no será encontrada."
msgid ""
"The date until which this short URL will be valid. If someone tries to "
"access it after this date, it will not be found."
msgstr ""
"La fecha hasta la cual será válida está URL acortada. Si alguien intenta "
"acceder a ella después de esta fecha, no será encontrada."
msgid "If provided, this slug will be used instead of generating a short code"
msgstr ""
"Si se proporciona, este slug será usado en vez de generar un código corto"
msgid "This will limit the number of visits for this short URL."
msgstr "Esto limitará el número de visitas a esta URL acortada."
#, fuzzy
#| msgid "A long URL was not provided. Which URL do you want to shorten?:"
msgid "A long URL was not provided. Which URL do you want to be shortened?"
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 long URL:"
msgstr "URL larga procesada:"
msgid "Generated short URL:"
msgstr "URL corta 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."
#, php-format
msgid ""
"Provided slug \"%s\" is already in use by another URL. Try with a different "
"one."
msgstr ""
"El slug proporcionado \"%s\" ya está siendo usado para otra URL. Prueba con "
"uno 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 proporcionó un código corto. ¿Qué código corto deseas usar?"
msgid "Referer"
msgstr "Origen"
msgid "Date"
msgstr "Fecha"
msgid "User agent"
msgstr "Agente de usuario"
msgid "Country"
msgstr "País"
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 ""
"A query used to filter results by searching for it on the longUrl and "
"shortCode fields"
msgstr ""
"Una consulta usada para filtrar el resultado buscándola en los campos "
"longUrl y shortCode"
msgid "A comma-separated list of tags to filter results"
msgstr "Una lista de etiquetas separadas por coma para filtrar el resultado"
msgid ""
"The field from which we want to order by. Pass ASC or DESC separated by a "
"comma"
msgstr ""
"El campo por el cual queremos ordernar. Pasa ASC o DESC separado por una coma"
msgid "Whether to display the tags or not"
msgstr "Si se desea mostrar las etiquetas o no"
msgid "Short code"
msgstr "Código corto"
msgid "Short URL"
msgstr "URL corta"
msgid "Long URL"
msgstr "URL larga"
msgid "Date created"
msgstr "Fecha de creación"
msgid "Visits count"
msgstr "Número de visitas"
msgid "Tags"
msgstr "Etiquetas"
msgid "Short URLs properly listed"
msgstr "URLs cortas listadas correctamente"
msgid "Continue with page"
msgstr "Continuar con la página"
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?"
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."
msgid "Creates one or more tags."
msgstr "Crea una o más etiquetas."
msgid "The name of the tags to create"
msgstr "El nombre de las etiquetas a crear"
msgid "You have to provide at least one tag name"
msgstr "Debes proporcionar al menos un nombre de etiqueta"
msgid "Tags properly created"
msgstr "Etiquetas correctamente creadas"
msgid "Deletes one or more tags."
msgstr "Elimina una o más etiquetas."
msgid "The name of the tags to delete"
msgstr "El nombre de las etiquetas a eliminar"
msgid "Tags properly deleted"
msgstr "Etiquetas correctamente eliminadas"
msgid "Lists existing tags."
msgstr "Lista las etiquetas existentes."
#, fuzzy
msgid "Name"
msgstr "Nombre"
msgid "No tags yet"
msgstr "Aún no hay etiquetas"
msgid "Renames one existing tag."
msgstr "Renombra una etiqueta existente."
msgid "Current name of the tag."
msgstr "Nombre actual de la etiqueta."
msgid "New name of the tag."
msgstr "Nuevo nombre de la etiqueta."
msgid "Tag properly renamed."
msgstr "Etiqueta correctamente renombrada."
#, php-format
msgid "A tag with name \"%s\" was not found"
msgstr "Una etiqueta con nombre \"%s\" no ha sido encontrada"
msgid "Processes visits where location is not set yet"
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
#, php-format
msgid "There is already an instance of the \"%s\" command in execution"
msgstr "Ya existe una instancia del comando \"%s\" en ejecución"
#, 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 "Ignored visit with no IP address"
msgstr "Ignorada visita sin dirección IP"
msgid "Processing IP"
msgstr "Procesando IP"
msgid "Ignored localhost address"
msgstr "Ignorada IP de localhost"
msgid "An error occurred while locating IP. Skipped"
msgstr "Se produjo un error al localizar la IP. Ignorado"
msgid "Updates the GeoLite2 database file used to geolocate IP addresses"
msgstr ""
"Actualiza el fichero de base de datos de GeoLite2 usado para geolocalizar "
"direcciones IP"
msgid ""
"The GeoLite2 database is updated first Tuesday every month, so this command "
"should be ideally run every first Wednesday"
msgstr ""
"La base de datos de GeoLite2 se actualiza el primer Martes de cada mes, por "
"lo que la opción ideal es ejecutar este comando cada primer miércoles de mes"
msgid "GeoLite2 database properly updated"
msgstr "Base de datos de GeoLite2 correctamente actualizada"
msgid "An error occurred while updating GeoLite2 database"
msgstr "Se produjo un error al actualizar la base de datos de GeoLite2"
#~ msgid "IP location resolver limit reached. Waiting %s seconds..."
#~ msgstr "Limite del localizador de IPs alcanzado. Esperando %s segundos..."
#~ msgid "Remote Address"
#~ msgstr "Dirección remota"
#~ msgid "Original URL"
#~ msgstr "URL original"
#~ msgid "You have reached last page"
#~ msgstr "Has alcanzado la última página"
#~ msgid "No URL found for short code \"%s\""
#~ msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
#~ msgid "Created tags"
#~ msgstr "Etiquetas creadas"
#~ msgid "Deleted tags"
#~ msgstr "Etiquetas eliminadas"

View File

@@ -10,34 +10,26 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
use function sprintf;
class DisableKeyCommand extends Command
{
public const NAME = 'api-key:disable';
/**
* @var ApiKeyServiceInterface
*/
/** @var ApiKeyServiceInterface */
private $apiKeyService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
public function __construct(ApiKeyServiceInterface $apiKeyService)
{
$this->apiKeyService = $apiKeyService;
$this->translator = $translator;
parent::__construct();
$this->apiKeyService = $apiKeyService;
}
protected function configure(): void
{
$this->setName(self::NAME)
->setDescription($this->translator->translate('Disables an API key.'))
->addArgument('apiKey', InputArgument::REQUIRED, $this->translator->translate('The API key to disable'));
->setDescription('Disables an API key.')
->addArgument('apiKey', InputArgument::REQUIRED, 'The API key to disable');
}
protected function execute(InputInterface $input, OutputInterface $output): void
@@ -47,9 +39,9 @@ class DisableKeyCommand extends Command
try {
$this->apiKeyService->disable($apiKey);
$io->success(sprintf($this->translator->translate('API key "%s" properly disabled'), $apiKey));
$io->success(sprintf('API key "%s" properly disabled', $apiKey));
} catch (InvalidArgumentException $e) {
$io->error(sprintf($this->translator->translate('API key "%s" does not exist.'), $apiKey));
$io->error(sprintf('API key "%s" does not exist.', $apiKey));
}
}
}

View File

@@ -10,39 +10,32 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
use function sprintf;
class GenerateKeyCommand extends Command
{
public const NAME = 'api-key:generate';
/**
* @var ApiKeyServiceInterface
*/
/** @var ApiKeyServiceInterface */
private $apiKeyService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
public function __construct(ApiKeyServiceInterface $apiKeyService)
{
$this->apiKeyService = $apiKeyService;
$this->translator = $translator;
parent::__construct();
}
protected function configure(): void
{
$this->setName(self::NAME)
->setDescription($this->translator->translate('Generates a new valid API key.'))
->addOption(
'expirationDate',
'e',
InputOption::VALUE_OPTIONAL,
$this->translator->translate('The date in which the API key should expire. Use any valid PHP format.')
);
$this
->setName(self::NAME)
->setDescription('Generates a new valid API key.')
->addOption(
'expirationDate',
'e',
InputOption::VALUE_OPTIONAL,
'The date in which the API key should expire. Use any valid PHP format.'
);
}
protected function execute(InputInterface $input, OutputInterface $output): void
@@ -50,8 +43,6 @@ class GenerateKeyCommand extends Command
$expirationDate = $input->getOption('expirationDate');
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? Chronos::parse($expirationDate) : null);
(new SymfonyStyle($input, $output))->success(
sprintf($this->translator->translate('Generated API key: "%s"'), $apiKey)
);
(new SymfonyStyle($input, $output))->success(sprintf('Generated API key: "%s"', $apiKey));
}
}

View File

@@ -10,7 +10,6 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
use function array_filter;
use function array_map;
use function sprintf;
@@ -23,32 +22,26 @@ class ListKeysCommand extends Command
public const NAME = 'api-key:list';
/**
* @var ApiKeyServiceInterface
*/
/** @var ApiKeyServiceInterface */
private $apiKeyService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
public function __construct(ApiKeyServiceInterface $apiKeyService)
{
$this->apiKeyService = $apiKeyService;
$this->translator = $translator;
parent::__construct();
$this->apiKeyService = $apiKeyService;
}
protected function configure(): void
{
$this->setName(self::NAME)
->setDescription($this->translator->translate('Lists all the available API keys.'))
->addOption(
'enabledOnly',
null,
InputOption::VALUE_NONE,
$this->translator->translate('Tells if only enabled API keys should be returned.')
);
$this
->setName(self::NAME)
->setDescription('Lists all the available API keys.')
->addOption(
'enabledOnly',
'e',
InputOption::VALUE_NONE,
'Tells if only enabled API keys should be returned.'
);
}
protected function execute(InputInterface $input, OutputInterface $output): void
@@ -70,9 +63,9 @@ class ListKeysCommand extends Command
}, $this->apiKeyService->listKeys($enabledOnly));
$io->table(array_filter([
$this->translator->translate('Key'),
! $enabledOnly ? $this->translator->translate('Is enabled') : null,
$this->translator->translate('Expiration date'),
'Key',
! $enabledOnly ? 'Is enabled' : null,
'Expiration date',
]), $rows);
}

View File

@@ -8,7 +8,6 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
use function sprintf;
use function str_shuffle;
@@ -16,31 +15,20 @@ class GenerateCharsetCommand extends Command
{
public const NAME = 'config:generate-charset';
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
parent::__construct();
}
protected function configure(): void
{
$this->setName(self::NAME)
->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));
$this
->setName(self::NAME)
->setDescription(sprintf(
'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
));
}
protected function execute(InputInterface $input, OutputInterface $output): void
{
$charSet = str_shuffle(UrlShortener::DEFAULT_CHARS);
(new SymfonyStyle($input, $output))->success(
sprintf($this->translator->translate('Character set: "%s"'), $charSet)
);
(new SymfonyStyle($input, $output))->success(sprintf('Character set: "%s"', $charSet));
}
}

View File

@@ -8,7 +8,6 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
use function sprintf;
class GenerateSecretCommand extends Command
@@ -17,30 +16,16 @@ class GenerateSecretCommand extends Command
public const NAME = 'config:generate-secret';
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
parent::__construct();
}
protected function configure(): void
{
$this->setName(self::NAME)
->setDescription($this->translator->translate(
'Generates a random secret string that can be used for JWT token encryption'
));
$this
->setName(self::NAME)
->setDescription('[DEPRECATED] Generates a random secret string that can be used for JWT token encryption');
}
protected function execute(InputInterface $input, OutputInterface $output): void
{
$secret = $this->generateRandomString(32);
(new SymfonyStyle($input, $output))->success(
sprintf($this->translator->translate('Secret key: "%s"'), $secret)
);
(new SymfonyStyle($input, $output))->success(sprintf('Secret key: "%s"', $secret));
}
}

View File

@@ -11,7 +11,6 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
use function sprintf;
class DeleteShortUrlCommand extends Command
@@ -19,20 +18,13 @@ class DeleteShortUrlCommand extends Command
public const NAME = 'short-url:delete';
private const ALIASES = ['short-code:delete'];
/**
* @var DeleteShortUrlServiceInterface
*/
/** @var DeleteShortUrlServiceInterface */
private $deleteShortUrlService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(DeleteShortUrlServiceInterface $deleteShortUrlService, TranslatorInterface $translator)
public function __construct(DeleteShortUrlServiceInterface $deleteShortUrlService)
{
$this->deleteShortUrlService = $deleteShortUrlService;
$this->translator = $translator;
parent::__construct();
$this->deleteShortUrlService = $deleteShortUrlService;
}
protected function configure(): void
@@ -40,22 +32,14 @@ class DeleteShortUrlCommand extends Command
$this
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription(
$this->translator->translate('Deletes a short URL')
)
->addArgument(
'shortCode',
InputArgument::REQUIRED,
$this->translator->translate('The short code for the short URL to be deleted')
)
->setDescription('Deletes a short URL')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code for the short URL to be deleted')
->addOption(
'ignore-threshold',
'i',
InputOption::VALUE_NONE,
$this->translator->translate(
'Ignores the safety visits threshold check, which could make short URLs with many visits to be '
. 'accidentally deleted'
)
'Ignores the safety visits threshold check, which could make short URLs with many visits to be '
. 'accidentally deleted'
);
}
@@ -68,9 +52,7 @@ class DeleteShortUrlCommand extends Command
try {
$this->runDelete($io, $shortCode, $ignoreThreshold);
} catch (Exception\InvalidShortCodeException $e) {
$io->error(
sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
);
$io->error(sprintf('Provided short code "%s" could not be found.', $shortCode));
} catch (Exception\DeleteShortUrlException $e) {
$this->retry($io, $shortCode, $e);
}
@@ -78,25 +60,24 @@ class DeleteShortUrlCommand extends Command
private function retry(SymfonyStyle $io, string $shortCode, Exception\DeleteShortUrlException $e): void
{
$warningMsg = sprintf($this->translator->translate(
'It was not possible to delete the short URL with short code "%s" because it has more than %s visits.'
), $shortCode, $e->getVisitsThreshold());
$warningMsg = sprintf(
'It was not possible to delete the short URL with short code "%s" because it has more than %s visits.',
$shortCode,
$e->getVisitsThreshold()
);
$io->writeln('<bg=yellow>' . $warningMsg . '</>');
$forceDelete = $io->confirm($this->translator->translate('Do you want to delete it anyway?'), false);
$forceDelete = $io->confirm('Do you want to delete it anyway?', false);
if ($forceDelete) {
$this->runDelete($io, $shortCode, true);
} else {
$io->warning($this->translator->translate('Short URL was not deleted.'));
$io->warning('Short URL was not deleted.');
}
}
private function runDelete(SymfonyStyle $io, string $shortCode, bool $ignoreThreshold): void
{
$this->deleteShortUrlService->deleteByShortCode($shortCode, $ignoreThreshold);
$io->success(sprintf(
$this->translator->translate('Short URL with short code "%s" successfully deleted.'),
$shortCode
));
$io->success(sprintf('Short URL with short code "%s" successfully deleted.', $shortCode));
}
}

View File

@@ -10,7 +10,6 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
use function sprintf;
class GeneratePreviewCommand extends Command
@@ -18,28 +17,16 @@ class GeneratePreviewCommand extends Command
public const NAME = 'short-url:process-previews';
private const ALIASES = ['shortcode:process-previews', 'short-code:process-previews'];
/**
* @var PreviewGeneratorInterface
*/
/** @var PreviewGeneratorInterface */
private $previewGenerator;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var ShortUrlServiceInterface
*/
/** @var ShortUrlServiceInterface */
private $shortUrlService;
public function __construct(
ShortUrlServiceInterface $shortUrlService,
PreviewGeneratorInterface $previewGenerator,
TranslatorInterface $translator
) {
public function __construct(ShortUrlServiceInterface $shortUrlService, PreviewGeneratorInterface $previewGenerator)
{
parent::__construct();
$this->shortUrlService = $shortUrlService;
$this->previewGenerator = $previewGenerator;
$this->translator = $translator;
parent::__construct(null);
}
protected function configure(): void
@@ -48,9 +35,7 @@ class GeneratePreviewCommand extends Command
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription(
$this->translator->translate(
'Processes and generates the previews for every URL, improving performance for later web requests.'
)
'Processes and generates the previews for every URL, improving performance for later web requests.'
);
}
@@ -66,17 +51,17 @@ class GeneratePreviewCommand extends Command
}
} while ($page <= $shortUrls->count());
(new SymfonyStyle($input, $output))->success($this->translator->translate('Finished processing all URLs'));
(new SymfonyStyle($input, $output))->success('Finished processing all URLs');
}
private function processUrl($url, OutputInterface $output): void
{
try {
$output->write(sprintf($this->translator->translate('Processing URL %s...'), $url));
$output->write(sprintf('Processing URL %s...', $url));
$this->previewGenerator->generatePreview($url);
$output->writeln($this->translator->translate(' <info>Success!</info>'));
$output->writeln(' <info>Success!</info>');
} catch (PreviewGenerationException $e) {
$output->writeln(' <error>' . $this->translator->translate('Error') . '</error>');
$output->writeln(' <error>Error</error>');
if ($output->isVerbose()) {
$this->getApplication()->renderException($e, $output);
}

View File

@@ -15,7 +15,6 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\Diactoros\Uri;
use Zend\I18n\Translator\TranslatorInterface;
use function array_merge;
use function explode;
use function sprintf;
@@ -27,28 +26,16 @@ class GenerateShortUrlCommand extends Command
public const NAME = 'short-url:generate';
private const ALIASES = ['shortcode:generate', 'short-code:generate'];
/**
* @var UrlShortenerInterface
*/
/** @var UrlShortenerInterface */
private $urlShortener;
/**
* @var array
*/
/** @var array */
private $domainConfig;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(
UrlShortenerInterface $urlShortener,
TranslatorInterface $translator,
array $domainConfig
) {
public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig)
{
parent::__construct();
$this->urlShortener = $urlShortener;
$this->translator = $translator;
$this->domainConfig = $domainConfig;
parent::__construct(null);
}
protected function configure(): void
@@ -56,30 +43,40 @@ class GenerateShortUrlCommand extends Command
$this
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription(
$this->translator->translate('Generates a short URL for provided long URL and returns it')
)
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
->setDescription('Generates a short URL for provided long URL and returns it')
->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse')
->addOption(
'tags',
't',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
$this->translator->translate('Tags to apply to the new short URL')
'Tags to apply to the new short URL'
)
->addOption('validSince', 's', InputOption::VALUE_REQUIRED, $this->translator->translate(
->addOption(
'validSince',
's',
InputOption::VALUE_REQUIRED,
'The date from which this short URL will be valid. '
. 'If someone tries to access it before this date, it will not be found.'
))
->addOption('validUntil', 'u', InputOption::VALUE_REQUIRED, $this->translator->translate(
)
->addOption(
'validUntil',
'u',
InputOption::VALUE_REQUIRED,
'The date until which this short URL will be valid. '
. 'If someone tries to access it after this date, it will not be found.'
))
->addOption('customSlug', 'c', InputOption::VALUE_REQUIRED, $this->translator->translate(
)
->addOption(
'customSlug',
'c',
InputOption::VALUE_REQUIRED,
'If provided, this slug will be used instead of generating a short code'
))
->addOption('maxVisits', 'm', InputOption::VALUE_REQUIRED, $this->translator->translate(
)
->addOption(
'maxVisits',
'm',
InputOption::VALUE_REQUIRED,
'This will limit the number of visits for this short URL.'
));
);
}
protected function interact(InputInterface $input, OutputInterface $output): void
@@ -90,9 +87,7 @@ class GenerateShortUrlCommand extends Command
return;
}
$longUrl = $io->ask(
$this->translator->translate('A long URL was not provided. Which URL do you want to be shortened?')
);
$longUrl = $io->ask('A long URL was not provided. Which URL do you want to be shortened?');
if (! empty($longUrl)) {
$input->setArgument('longUrl', $longUrl);
}
@@ -103,7 +98,7 @@ class GenerateShortUrlCommand extends Command
$io = new SymfonyStyle($input, $output);
$longUrl = $input->getArgument('longUrl');
if (empty($longUrl)) {
$io->error($this->translator->translate('A URL was not provided!'));
$io->error('A URL was not provided!');
return;
}
@@ -129,21 +124,15 @@ class GenerateShortUrlCommand extends Command
$shortUrl = $this->buildShortUrl($this->domainConfig, $shortCode);
$io->writeln([
sprintf('%s <info>%s</info>', $this->translator->translate('Processed long URL:'), $longUrl),
sprintf('%s <info>%s</info>', $this->translator->translate('Generated short URL:'), $shortUrl),
sprintf('Processed long URL: <info>%s</info>', $longUrl),
sprintf('Generated short URL: <info>%s</info>', $shortUrl),
]);
} catch (InvalidUrlException $e) {
$io->error(sprintf(
$this->translator->translate('Provided URL "%s" is invalid. Try with a different one.'),
$longUrl
));
$io->error(sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl));
} catch (NonUniqueSlugException $e) {
$io->error(sprintf(
$this->translator->translate(
'Provided slug "%s" is already in use by another URL. Try with a different one.'
),
$customSlug
));
$io->error(
sprintf('Provided slug "%s" is already in use by another URL. Try with a different one.', $customSlug)
);
}
}

View File

@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
@@ -13,7 +14,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
use Zend\Stdlib\ArrayUtils;
use function array_map;
use function Functional\select_keys;
@@ -22,19 +23,12 @@ class GetVisitsCommand extends Command
public const NAME = 'short-url:visits';
private const ALIASES = ['shortcode:visits', 'short-code:visits'];
/**
* @var VisitsTrackerInterface
*/
/** @var VisitsTrackerInterface */
private $visitsTracker;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator)
public function __construct(VisitsTrackerInterface $visitsTracker)
{
$this->visitsTracker = $visitsTracker;
$this->translator = $translator;
parent::__construct();
}
@@ -43,25 +37,19 @@ class GetVisitsCommand extends Command
$this
->setName(self::NAME)
->setAliases(self::ALIASES)
->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')
)
->setDescription('Returns the detailed visits information for provided short code')
->addArgument('shortCode', InputArgument::REQUIRED, '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')
'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')
'Allows to filter visits, returning only those newer than end date'
);
}
@@ -73,9 +61,7 @@ class GetVisitsCommand extends Command
}
$io = new SymfonyStyle($input, $output);
$shortCode = $io->ask(
$this->translator->translate('A short code was not provided. Which short code do you want to use?')
);
$shortCode = $io->ask('A short code was not provided. Which short code do you want to use?');
if (! empty($shortCode)) {
$input->setArgument('shortCode', $shortCode);
}
@@ -88,18 +74,15 @@ class GetVisitsCommand extends Command
$startDate = $this->getDateOption($input, 'startDate');
$endDate = $this->getDateOption($input, 'endDate');
$visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate));
$paginator = $this->visitsTracker->info($shortCode, new VisitsParams(new DateRange($startDate, $endDate)));
$visits = ArrayUtils::iteratorToArray($paginator->getCurrentItems());
$rows = array_map(function (Visit $visit) {
$rowData = $visit->jsonSerialize();
$rowData['country'] = $visit->getVisitLocation()->getCountryName();
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country']);
}, $visits);
$io->table([
$this->translator->translate('Referer'),
$this->translator->translate('Date'),
$this->translator->translate('User agent'),
$this->translator->translate('Country'),
], $rows);
$io->table(['Referer', 'Date', 'User agent', 'Country'], $rows);
}
private function getDateOption(InputInterface $input, $key)

View File

@@ -12,7 +12,6 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
use function array_values;
use function count;
use function explode;
@@ -26,27 +25,15 @@ class ListShortUrlsCommand extends Command
public const NAME = 'short-url:list';
private const ALIASES = ['shortcode:list', 'short-code:list'];
/**
* @var ShortUrlServiceInterface
*/
/** @var ShortUrlServiceInterface */
private $shortUrlService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var array
*/
/** @var array */
private $domainConfig;
public function __construct(
ShortUrlServiceInterface $shortUrlService,
TranslatorInterface $translator,
array $domainConfig
) {
$this->shortUrlService = $shortUrlService;
$this->translator = $translator;
public function __construct(ShortUrlServiceInterface $shortUrlService, array $domainConfig)
{
parent::__construct();
$this->shortUrlService = $shortUrlService;
$this->domainConfig = $domainConfig;
}
@@ -55,45 +42,33 @@ class ListShortUrlsCommand extends Command
$this
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription($this->translator->translate('List all short URLs'))
->setDescription('List all short URLs')
->addOption(
'page',
'p',
InputOption::VALUE_OPTIONAL,
sprintf(
$this->translator->translate('The first page to list (%s items per page)'),
PaginableRepositoryAdapter::ITEMS_PER_PAGE
),
sprintf('The first page to list (%s items per page)', PaginableRepositoryAdapter::ITEMS_PER_PAGE),
'1'
)
->addOption(
'searchTerm',
's',
InputOption::VALUE_OPTIONAL,
$this->translator->translate(
'A query used to filter results by searching for it on the longUrl and shortCode fields'
)
'A query used to filter results by searching for it on the longUrl and shortCode fields'
)
->addOption(
'tags',
't',
InputOption::VALUE_OPTIONAL,
$this->translator->translate('A comma-separated list of tags to filter results')
'A comma-separated list of tags to filter results'
)
->addOption(
'orderBy',
'o',
InputOption::VALUE_OPTIONAL,
$this->translator->translate(
'The field from which we want to order by. Pass ASC or DESC separated by a comma'
)
'The field from which we want to order by. Pass ASC or DESC separated by a comma'
)
->addOption(
'showTags',
null,
InputOption::VALUE_NONE,
$this->translator->translate('Whether to display the tags or not')
);
->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags or not');
}
protected function execute(InputInterface $input, OutputInterface $output): void
@@ -110,15 +85,9 @@ class ListShortUrlsCommand extends Command
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
$page++;
$headers = [
$this->translator->translate('Short code'),
$this->translator->translate('Short URL'),
$this->translator->translate('Long URL'),
$this->translator->translate('Date created'),
$this->translator->translate('Visits count'),
];
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
if ($showTags) {
$headers[] = $this->translator->translate('Tags');
$headers[] = 'Tags';
}
$rows = [];
@@ -137,12 +106,9 @@ class ListShortUrlsCommand extends Command
if ($this->isLastPage($result)) {
$continue = false;
$io->success($this->translator->translate('Short URLs properly listed'));
$io->success('Short URLs properly listed');
} else {
$continue = $io->confirm(
sprintf($this->translator->translate('Continue with page') . ' <options=bold>%s</>?', $page),
false
);
$continue = $io->confirm(sprintf('Continue with page <options=bold>%s</>?', $page), false);
}
} while ($continue);
}

View File

@@ -11,7 +11,6 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
use function sprintf;
class ResolveUrlCommand extends Command
@@ -19,20 +18,13 @@ class ResolveUrlCommand extends Command
public const NAME = 'short-url:parse';
private const ALIASES = ['shortcode:parse', 'short-code:parse'];
/**
* @var UrlShortenerInterface
*/
/** @var UrlShortenerInterface */
private $urlShortener;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator)
public function __construct(UrlShortenerInterface $urlShortener)
{
parent::__construct();
$this->urlShortener = $urlShortener;
$this->translator = $translator;
parent::__construct(null);
}
protected function configure(): void
@@ -40,12 +32,8 @@ class ResolveUrlCommand extends Command
$this
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription($this->translator->translate('Returns the long URL behind a short code'))
->addArgument(
'shortCode',
InputArgument::REQUIRED,
$this->translator->translate('The short code to parse')
);
->setDescription('Returns the long URL behind a short code')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse');
}
protected function interact(InputInterface $input, OutputInterface $output): void
@@ -56,9 +44,7 @@ class ResolveUrlCommand extends Command
}
$io = new SymfonyStyle($input, $output);
$shortCode = $io->ask(
$this->translator->translate('A short code was not provided. Which short code do you want to parse?')
);
$shortCode = $io->ask('A short code was not provided. Which short code do you want to parse?');
if (! empty($shortCode)) {
$input->setArgument('shortCode', $shortCode);
}
@@ -71,17 +57,11 @@ class ResolveUrlCommand extends Command
try {
$url = $this->urlShortener->shortCodeToUrl($shortCode);
$output->writeln(
sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $url->getLongUrl())
);
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
} catch (InvalidShortCodeException $e) {
$io->error(
sprintf($this->translator->translate('Provided short code "%s" has an invalid format.'), $shortCode)
);
$io->error(sprintf('Provided short code "%s" has an invalid format.', $shortCode));
} catch (EntityDoesNotExistException $e) {
$io->error(
sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
);
$io->error(sprintf('Provided short code "%s" could not be found.', $shortCode));
}
}
}

View File

@@ -9,38 +9,30 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
class CreateTagCommand extends Command
{
public const NAME = 'tag:create';
/**
* @var TagServiceInterface
*/
/** @var TagServiceInterface */
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
public function __construct(TagServiceInterface $tagService)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
$this->tagService = $tagService;
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription($this->translator->translate('Creates one or more tags.'))
->setDescription('Creates one or more tags.')
->addOption(
'name',
't',
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
$this->translator->translate('The name of the tags to create')
'The name of the tags to create'
);
}
@@ -50,11 +42,11 @@ class CreateTagCommand extends Command
$tagNames = $input->getOption('name');
if (empty($tagNames)) {
$io->warning($this->translator->translate('You have to provide at least one tag name'));
$io->warning('You have to provide at least one tag name');
return;
}
$this->tagService->createTags($tagNames);
$io->success($this->translator->translate('Tags properly created'));
$io->success('Tags properly created');
}
}

View File

@@ -9,38 +9,30 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
class DeleteTagsCommand extends Command
{
public const NAME = 'tag:delete';
/**
* @var TagServiceInterface
*/
/** @var TagServiceInterface */
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
public function __construct(TagServiceInterface $tagService)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
$this->tagService = $tagService;
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription($this->translator->translate('Deletes one or more tags.'))
->setDescription('Deletes one or more tags.')
->addOption(
'name',
't',
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
$this->translator->translate('The name of the tags to delete')
'The name of the tags to delete'
);
}
@@ -50,11 +42,11 @@ class DeleteTagsCommand extends Command
$tagNames = $input->getOption('name');
if (empty($tagNames)) {
$io->warning($this->translator->translate('You have to provide at least one tag name'));
$io->warning('You have to provide at least one tag name');
return;
}
$this->tagService->deleteTags($tagNames);
$io->success($this->translator->translate('Tags properly deleted'));
$io->success('Tags properly deleted');
}
}

View File

@@ -9,47 +9,39 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
use function Functional\map;
class ListTagsCommand extends Command
{
public const NAME = 'tag:list';
/**
* @var TagServiceInterface
*/
/** @var TagServiceInterface */
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
public function __construct(TagServiceInterface $tagService)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
$this->tagService = $tagService;
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription($this->translator->translate('Lists existing tags.'));
->setDescription('Lists existing tags.');
}
protected function execute(InputInterface $input, OutputInterface $output): void
{
$io = new SymfonyStyle($input, $output);
$io->table([$this->translator->translate('Name')], $this->getTagsRows());
$io->table(['Name'], $this->getTagsRows());
}
private function getTagsRows(): array
{
$tags = $this->tagService->listTags();
if (empty($tags)) {
return [[$this->translator->translate('No tags yet')]];
return [['No tags yet']];
}
return map($tags, function (Tag $tag) {

View File

@@ -10,36 +10,28 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
use function sprintf;
class RenameTagCommand extends Command
{
public const NAME = 'tag:rename';
/**
* @var TagServiceInterface
*/
/** @var TagServiceInterface */
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
public function __construct(TagServiceInterface $tagService)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
$this->tagService = $tagService;
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription($this->translator->translate('Renames one existing tag.'))
->addArgument('oldName', InputArgument::REQUIRED, $this->translator->translate('Current name of the tag.'))
->addArgument('newName', InputArgument::REQUIRED, $this->translator->translate('New name of the tag.'));
->setDescription('Renames one existing tag.')
->addArgument('oldName', InputArgument::REQUIRED, 'Current name of the tag.')
->addArgument('newName', InputArgument::REQUIRED, 'New name of the tag.');
}
protected function execute(InputInterface $input, OutputInterface $output): void
@@ -50,9 +42,9 @@ class RenameTagCommand extends Command
try {
$this->tagService->renameTag($oldName, $newName);
$io->success($this->translator->translate('Tag properly renamed.'));
$io->success('Tag properly renamed.');
} catch (EntityDoesNotExistException $e) {
$io->error(sprintf($this->translator->translate('A tag with name "%s" was not found'), $oldName));
$io->error(sprintf('A tag with name "%s" was not found', $oldName));
}
}
}

View File

@@ -15,52 +15,37 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Lock\Factory as Locker;
use Zend\I18n\Translator\TranslatorInterface;
use function sprintf;
class ProcessVisitsCommand extends Command
{
public const NAME = 'visit:process';
/**
* @var VisitServiceInterface
*/
/** @var VisitServiceInterface */
private $visitService;
/**
* @var IpLocationResolverInterface
*/
/** @var IpLocationResolverInterface */
private $ipLocationResolver;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var Locker
*/
/** @var Locker */
private $locker;
/**
* @var OutputInterface
*/
/** @var OutputInterface */
private $output;
public function __construct(
VisitServiceInterface $visitService,
IpLocationResolverInterface $ipLocationResolver,
Locker $locker,
TranslatorInterface $translator
Locker $locker
) {
parent::__construct();
$this->visitService = $visitService;
$this->ipLocationResolver = $ipLocationResolver;
$this->translator = $translator;
$this->locker = $locker;
parent::__construct();
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription($this->translator->translate('Processes visits where location is not set yet'));
->setDescription('Processes visits where location is not set yet');
}
protected function execute(InputInterface $input, OutputInterface $output): void
@@ -70,10 +55,7 @@ class ProcessVisitsCommand extends Command
$lock = $this->locker->createLock(self::NAME);
if (! $lock->acquire()) {
$io->warning(sprintf(
$this->translator->translate('There is already an instance of the "%s" command in execution'),
self::NAME
));
$io->warning(sprintf('There is already an instance of the "%s" command in execution', self::NAME));
return;
}
@@ -81,14 +63,11 @@ class ProcessVisitsCommand extends Command
$this->visitService->locateVisits(
[$this, 'getGeolocationDataForVisit'],
function (VisitLocation $location) use ($output) {
$output->writeln(sprintf(
' [<info>' . $this->translator->translate('Address located at "%s"') . '</info>]',
$location->getCountryName()
));
$output->writeln(sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName()));
}
);
$io->success($this->translator->translate('Finished processing all IPs'));
$io->success('Finished processing all IPs');
} finally {
$lock->release();
}
@@ -97,31 +76,24 @@ class ProcessVisitsCommand extends Command
public function getGeolocationDataForVisit(Visit $visit): array
{
if (! $visit->hasRemoteAddr()) {
$this->output->writeln(sprintf(
'<comment>%s</comment>',
$this->translator->translate('Ignored visit with no IP address')
), OutputInterface::VERBOSITY_VERBOSE);
$this->output->writeln(
'<comment>Ignored visit with no IP address</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
throw new IpCannotBeLocatedException('Ignored visit with no IP address');
}
$ipAddr = $visit->getRemoteAddr();
$this->output->write(sprintf('%s <fg=blue>%s</>', $this->translator->translate('Processing IP'), $ipAddr));
$this->output->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
if ($ipAddr === IpAddress::LOCALHOST) {
$this->output->writeln(
sprintf(' [<comment>%s</comment>]', $this->translator->translate('Ignored localhost address'))
);
$this->output->writeln(' [<comment>Ignored localhost address</comment>]');
throw new IpCannotBeLocatedException('Ignored localhost address');
}
try {
return $this->ipLocationResolver->resolveIpLocation($ipAddr);
} catch (WrongIpException $e) {
$this->output->writeln(
sprintf(
' [<fg=red>%s</>]',
$this->translator->translate('An error occurred while locating IP. Skipped')
)
);
$this->output->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
if ($this->output->isVerbose()) {
$this->getApplication()->renderException($e, $this->output);
}

View File

@@ -10,39 +10,29 @@ use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
class UpdateDbCommand extends Command
{
public const NAME = 'visit:update-db';
/**
* @var DbUpdaterInterface
*/
/** @var DbUpdaterInterface */
private $geoLiteDbUpdater;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(DbUpdaterInterface $geoLiteDbUpdater, TranslatorInterface $translator)
public function __construct(DbUpdaterInterface $geoLiteDbUpdater)
{
$this->geoLiteDbUpdater = $geoLiteDbUpdater;
$this->translator = $translator;
parent::__construct();
$this->geoLiteDbUpdater = $geoLiteDbUpdater;
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription(
$this->translator->translate('Updates the GeoLite2 database file used to geolocate IP addresses')
)
->setHelp($this->translator->translate(
->setDescription('Updates the GeoLite2 database file used to geolocate IP addresses')
->setHelp(
'The GeoLite2 database is updated first Tuesday every month, so this command should be ideally run '
. 'every first Wednesday'
));
);
}
protected function execute(InputInterface $input, OutputInterface $output): void
@@ -60,12 +50,12 @@ class UpdateDbCommand extends Command
$progressBar->finish();
$io->writeln('');
$io->success($this->translator->translate('GeoLite2 database properly updated'));
$io->success('GeoLite2 database properly updated');
} catch (RuntimeException $e) {
$progressBar->finish();
$io->writeln('');
$io->error($this->translator->translate('An error occurred while updating GeoLite2 database'));
$io->error('An error occurred while updating GeoLite2 database');
if ($io->isVerbose()) {
$this->getApplication()->renderException($e, $output);
}

View File

@@ -10,7 +10,6 @@ use Psr\Container\NotFoundExceptionInterface;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Symfony\Component\Console\Application as CliApp;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
@@ -34,8 +33,6 @@ class ApplicationFactory implements FactoryInterface
{
$config = $container->get('config')['cli'];
$appOptions = $container->get(AppOptions::class);
$translator = $container->get(Translator::class);
$translator->setLocale($config['locale']);
$commands = $config['commands'] ?? [];
$app = new CliApp($appOptions->getName(), $appOptions->getVersion());

View File

@@ -10,23 +10,18 @@ use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class DisableKeyCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $apiKeyService;
/** @var CommandTester */
private $commandTester;
/** @var ObjectProphecy */
private $apiKeyService;
public function setUp()
{
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
$command = new DisableKeyCommand($this->apiKeyService->reveal(), Translator::factory([]));
$command = new DisableKeyCommand($this->apiKeyService->reveal());
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);

View File

@@ -12,23 +12,18 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class GenerateKeyCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $apiKeyService;
/** @var CommandTester */
private $commandTester;
/** @var ObjectProphecy */
private $apiKeyService;
public function setUp()
{
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
$command = new GenerateKeyCommand($this->apiKeyService->reveal(), Translator::factory([]));
$command = new GenerateKeyCommand($this->apiKeyService->reveal());
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);

View File

@@ -10,23 +10,18 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class ListKeysCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $apiKeyService;
/** @var CommandTester */
private $commandTester;
/** @var ObjectProphecy */
private $apiKeyService;
public function setUp()
{
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
$command = new ListKeysCommand($this->apiKeyService->reveal(), Translator::factory([]));
$command = new ListKeysCommand($this->apiKeyService->reveal());
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);

View File

@@ -7,21 +7,18 @@ use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
use function implode;
use function sort;
use function str_split;
class GenerateCharsetCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/** @var CommandTester */
private $commandTester;
public function setUp()
{
$command = new GenerateCharsetCommand(Translator::factory([]));
$command = new GenerateCharsetCommand();
$app = new Application();
$app->add($command);

View File

@@ -11,26 +11,21 @@ use Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
use function array_pop;
use function sprintf;
class DeleteShortCodeCommandTest extends TestCase
{
/**
* @var CommandTester
*/
/** @var CommandTester */
private $commandTester;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $service;
public function setUp()
{
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
$command = new DeleteShortUrlCommand($this->service->reveal(), Translator::factory([]));
$command = new DeleteShortUrlCommand($this->service->reveal());
$app = new Application();
$app->add($command);

View File

@@ -13,7 +13,6 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
use Zend\Paginator\Adapter\ArrayAdapter;
use Zend\Paginator\Paginator;
use function count;
@@ -21,17 +20,11 @@ use function substr_count;
class GeneratePreviewCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
/** @var CommandTester */
private $commandTester;
/** @var ObjectProphecy */
private $previewGenerator;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $shortUrlService;
public function setUp()
@@ -39,11 +32,7 @@ class GeneratePreviewCommandTest extends TestCase
$this->previewGenerator = $this->prophesize(PreviewGenerator::class);
$this->shortUrlService = $this->prophesize(ShortUrlService::class);
$command = new GeneratePreviewCommand(
$this->shortUrlService->reveal(),
$this->previewGenerator->reveal(),
Translator::factory([])
);
$command = new GeneratePreviewCommand($this->shortUrlService->reveal(), $this->previewGenerator->reveal());
$app = new Application();
$app->add($command);

View File

@@ -14,23 +14,18 @@ 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 GenerateShortUrlCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $urlShortener;
/** @var CommandTester */
private $commandTester;
/** @var ObjectProphecy */
private $urlShortener;
public function setUp()
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), Translator::factory([]), [
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), [
'schema' => 'http',
'hostname' => 'foo.com',
]);

View File

@@ -13,27 +13,24 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
use function strpos;
use Zend\Paginator\Adapter\ArrayAdapter;
use Zend\Paginator\Paginator;
class GetVisitsCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $visitsTracker;
/** @var CommandTester */
private $commandTester;
/** @var ObjectProphecy */
private $visitsTracker;
public function setUp()
{
$this->visitsTracker = $this->prophesize(VisitsTrackerInterface::class);
$command = new GetVisitsCommand($this->visitsTracker->reveal(), Translator::factory([]));
$command = new GetVisitsCommand($this->visitsTracker->reveal());
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
@@ -45,8 +42,9 @@ class GetVisitsCommandTest extends TestCase
public function noDateFlagsTriesToListWithoutDateRange()
{
$shortCode = 'abc123';
$this->visitsTracker->info($shortCode, new DateRange(null, null))->willReturn([])
->shouldBeCalledOnce();
$this->visitsTracker->info($shortCode, new VisitsParams(new DateRange(null, null)))->willReturn(
new Paginator(new ArrayAdapter([]))
)->shouldBeCalledOnce();
$this->commandTester->execute([
'command' => 'shortcode:visits',
@@ -62,8 +60,11 @@ class GetVisitsCommandTest extends TestCase
$shortCode = 'abc123';
$startDate = '2016-01-01';
$endDate = '2016-02-01';
$this->visitsTracker->info($shortCode, new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)))
->willReturn([])
$this->visitsTracker->info(
$shortCode,
new VisitsParams(new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)))
)
->willReturn(new Paginator(new ArrayAdapter([])))
->shouldBeCalledOnce();
$this->commandTester->execute([
@@ -80,19 +81,21 @@ class GetVisitsCommandTest extends TestCase
public function outputIsProperlyGenerated()
{
$shortCode = 'abc123';
$this->visitsTracker->info($shortCode, Argument::any())->willReturn([
(new Visit(new ShortUrl(''), new Visitor('bar', 'foo', '')))->locate(
new VisitLocation(['country_name' => 'Spain'])
),
])->shouldBeCalledOnce();
$this->visitsTracker->info($shortCode, Argument::any())->willReturn(
new Paginator(new ArrayAdapter([
(new Visit(new ShortUrl(''), new Visitor('bar', 'foo', '')))->locate(
new VisitLocation(['country_name' => 'Spain'])
),
]))
)->shouldBeCalledOnce();
$this->commandTester->execute([
'command' => 'shortcode:visits',
'shortCode' => $shortCode,
]);
$output = $this->commandTester->getDisplay();
$this->assertGreaterThan(0, strpos($output, 'foo'));
$this->assertGreaterThan(0, strpos($output, 'Spain'));
$this->assertGreaterThan(0, strpos($output, 'bar'));
$this->assertContains('foo', $output);
$this->assertContains('Spain', $output);
$this->assertContains('bar', $output);
}
}

View File

@@ -11,26 +11,21 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
use Zend\Paginator\Adapter\ArrayAdapter;
use Zend\Paginator\Paginator;
class ListShortUrlsCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $shortUrlService;
/** @var CommandTester */
private $commandTester;
/** @var ObjectProphecy */
private $shortUrlService;
public function setUp()
{
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
$app = new Application();
$command = new ListShortUrlsCommand($this->shortUrlService->reveal(), Translator::factory([]), []);
$command = new ListShortUrlsCommand($this->shortUrlService->reveal(), []);
$app->add($command);
$this->commandTester = new CommandTester($command);
}

View File

@@ -12,24 +12,19 @@ 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;
use const PHP_EOL;
class ResolveUrlCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $urlShortener;
/** @var CommandTester */
private $commandTester;
/** @var ObjectProphecy */
private $urlShortener;
public function setUp()
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$command = new ResolveUrlCommand($this->urlShortener->reveal(), Translator::factory([]));
$command = new ResolveUrlCommand($this->urlShortener->reveal());
$app = new Application();
$app->add($command);

View File

@@ -11,24 +11,19 @@ use Shlinkio\Shlink\CLI\Command\Tag\CreateTagCommand;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class CreateTagCommandTest extends TestCase
{
/**
* @var CommandTester
*/
/** @var CommandTester */
private $commandTester;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $tagService;
public function setUp()
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$command = new CreateTagCommand($this->tagService->reveal(), Translator::factory([]));
$command = new CreateTagCommand($this->tagService->reveal());
$app = new Application();
$app->add($command);

View File

@@ -10,28 +10,21 @@ use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class DeleteTagsCommandTest extends TestCase
{
/**
* @var DeleteTagsCommand
*/
/** @var DeleteTagsCommand */
private $command;
/**
* @var CommandTester
*/
/** @var CommandTester */
private $commandTester;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $tagService;
public function setUp()
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$command = new DeleteTagsCommand($this->tagService->reveal(), Translator::factory([]));
$command = new DeleteTagsCommand($this->tagService->reveal());
$app = new Application();
$app->add($command);

View File

@@ -11,28 +11,21 @@ use Shlinkio\Shlink\Core\Entity\Tag;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class ListTagsCommandTest extends TestCase
{
/**
* @var ListTagsCommand
*/
/** @var ListTagsCommand */
private $command;
/**
* @var CommandTester
*/
/** @var CommandTester */
private $commandTester;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $tagService;
public function setUp()
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$command = new ListTagsCommand($this->tagService->reveal(), Translator::factory([]));
$command = new ListTagsCommand($this->tagService->reveal());
$app = new Application();
$app->add($command);

View File

@@ -12,28 +12,21 @@ use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class RenameTagCommandTest extends TestCase
{
/**
* @var RenameTagCommand
*/
/** @var RenameTagCommand */
private $command;
/**
* @var CommandTester
*/
/** @var CommandTester */
private $commandTester;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $tagService;
public function setUp()
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$command = new RenameTagCommand($this->tagService->reveal(), Translator::factory([]));
$command = new RenameTagCommand($this->tagService->reveal());
$app = new Application();
$app->add($command);

View File

@@ -21,31 +21,20 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Lock;
use Throwable;
use Zend\I18n\Translator\Translator;
use function array_shift;
use function sprintf;
class ProcessVisitsCommandTest extends TestCase
{
/**
* @var CommandTester
*/
/** @var CommandTester */
private $commandTester;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $visitService;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $ipResolver;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $locker;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $lock;
public function setUp()
@@ -63,8 +52,7 @@ class ProcessVisitsCommandTest extends TestCase
$command = new ProcessVisitsCommand(
$this->visitService->reveal(),
$this->ipResolver->reveal(),
$this->locker->reveal(),
Translator::factory([])
$this->locker->reveal()
);
$app = new Application();
$app->add($command);

View File

@@ -11,24 +11,19 @@ use Shlinkio\Shlink\Common\Exception\RuntimeException;
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class UpdateDbCommandTest extends TestCase
{
/**
* @var CommandTester
*/
/** @var CommandTester */
private $commandTester;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $dbUpdater;
public function setUp()
{
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
$command = new UpdateDbCommand($this->dbUpdater->reveal(), Translator::factory([]));
$command = new UpdateDbCommand($this->dbUpdater->reveal());
$app = new Application();
$app->add($command);

View File

@@ -8,10 +8,8 @@ use Shlinkio\Shlink\CLI\ConfigProvider;
class ConfigProviderTest extends TestCase
{
/**
* @var ConfigProvider
*/
protected $configProvider;
/** @var ConfigProvider */
private $configProvider;
public function setUp()
{
@@ -23,10 +21,9 @@ class ConfigProviderTest extends TestCase
*/
public function confiIsProperlyReturned()
{
$config = $this->configProvider->__invoke();
$config = ($this->configProvider)();
$this->assertArrayHasKey('cli', $config);
$this->assertArrayHasKey('dependencies', $config);
$this->assertArrayHasKey('translator', $config);
}
}

View File

@@ -10,16 +10,13 @@ use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\ServiceManager;
use function array_merge;
class ApplicationFactoryTest extends TestCase
{
/**
* @var ApplicationFactory
*/
protected $factory;
/** @var ApplicationFactory */
private $factory;
public function setUp()
{
@@ -66,7 +63,6 @@ class ApplicationFactoryTest extends TestCase
'cli' => array_merge($config, ['locale' => 'en']),
],
AppOptions::class => new AppOptions(),
Translator::class => Translator::factory([]),
]]);
}

View File

@@ -23,7 +23,6 @@ return [
EntityManager::class => Factory\EntityManagerFactory::class,
GuzzleClient::class => InvokableFactory::class,
Cache::class => Factory\CacheFactory::class,
'Logger_Shlink' => Factory\LoggerFactory::class,
Filesystem::class => InvokableFactory::class,
Reader::class => ConfigAbstractFactory::class,
@@ -31,6 +30,7 @@ return [
Template\Extension\TranslatorExtension::class => ConfigAbstractFactory::class,
Middleware\LocaleMiddleware::class => ConfigAbstractFactory::class,
Middleware\CloseDbConnectionMiddleware::class => ConfigAbstractFactory::class,
IpAddress::class => Middleware\IpAddressMiddlewareFactory::class,
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
@@ -78,6 +78,7 @@ return [
Template\Extension\TranslatorExtension::class => ['translator'],
Middleware\LocaleMiddleware::class => ['translator'],
Middleware\CloseDbConnectionMiddleware::class => ['em'],
IpGeolocation\IpApiLocationResolver::class => ['httpClient'],
IpGeolocation\GeoLite2LocationResolver::class => [Reader::class],

View File

@@ -9,9 +9,7 @@ use function is_array;
final class PathCollection
{
/**
* @var array
*/
/** @var array */
private $array;
public function __construct(array $array)

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Exec;
use const PHP_SAPI;
use function Shlinkio\Shlink\Common\env;
abstract class ExecutionContext
{
public const WEB = 'shlink_web';
public const CLI = 'shlink_cli';
public static function currentContextIsSwoole(): bool
{
return PHP_SAPI === 'cli' && env('CURRENT_SHLINK_CONTEXT', self::WEB) === self::WEB;
}
}

View File

@@ -27,6 +27,6 @@ class TranslatorFactory implements FactoryInterface
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->get('config');
return Translator::factory(isset($config['translator']) ? $config['translator'] : []);
return Translator::factory($config['translator'] ?? []);
}
}

View File

@@ -7,9 +7,7 @@ use Shlinkio\Shlink\Common\Exception\WrongIpException;
class ChainIpLocationResolver implements IpLocationResolverInterface
{
/**
* @var IpLocationResolverInterface[]
*/
/** @var IpLocationResolverInterface[] */
private $resolvers;
public function __construct(IpLocationResolverInterface ...$resolvers)

View File

@@ -19,17 +19,11 @@ class DbUpdater implements DbUpdaterInterface
private const DB_COMPRESSED_FILE = 'GeoLite2-City.tar.gz';
private const DB_DECOMPRESSED_FILE = 'GeoLite2-City.mmdb';
/**
* @var ClientInterface
*/
/** @var ClientInterface */
private $httpClient;
/**
* @var Filesystem
*/
/** @var Filesystem */
private $filesystem;
/**
* @var GeoLite2Options
*/
/** @var GeoLite2Options */
private $options;
public function __construct(ClientInterface $httpClient, Filesystem $filesystem, GeoLite2Options $options)

View File

@@ -13,9 +13,7 @@ use function Functional\first;
class GeoLite2LocationResolver implements IpLocationResolverInterface
{
/**
* @var Reader
*/
/** @var Reader */
private $geoLiteDbReader;
public function __construct(Reader $geoLiteDbReader)

View File

@@ -14,9 +14,7 @@ class IpApiLocationResolver implements IpLocationResolverInterface
{
private const SERVICE_PATTERN = 'http://ip-api.com/json/%s';
/**
* @var Client
*/
/** @var Client */
private $httpClient;
public function __construct(Client $httpClient)

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Logger\Swoole;
use Interop\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Zend\Expressive\Swoole\Log;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class AccessLogFactory 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.
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->has('config') ? $container->get('config') : [];
$config = $config['zend-expressive-swoole']['swoole-http-server']['logger'] ?? [];
return new Log\Psr3AccessLogDecorator(
$this->getLogger($container, $config),
$this->getFormatter($container, $config),
$config['use-hostname-lookups'] ?? false
);
}
private function getLogger(ContainerInterface $container, array $config): LoggerInterface
{
$loggerName = $config['logger_name'] ?? LoggerInterface::class;
return $container->has($loggerName) ? $container->get($loggerName) : new Log\StdoutLogger();
}
private function getFormatter(ContainerInterface $container, array $config): Log\AccessLogFormatterInterface
{
if ($container->has(Log\AccessLogFormatterInterface::class)) {
return $container->get(Log\AccessLogFormatterInterface::class);
}
return new Log\AccessLogFormatter($config['format'] ?? Log\AccessLogFormatter::FORMAT_COMMON);
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Middleware;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class CloseDbConnectionMiddleware implements MiddlewareInterface
{
/** @var EntityManagerInterface */
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* Process an incoming server request and return a response, optionally delegating
* response creation to a handler.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$handledRequest = $handler->handle($request);
$this->em->getConnection()->close();
$this->em->clear();
return $handledRequest;
}
}

View File

@@ -13,9 +13,9 @@ use function explode;
class LocaleMiddleware implements MiddlewareInterface
{
/**
* @var Translator
*/
private const ACCEPT_LANGUAGE = 'Accept-Language';
/** @var Translator */
private $translator;
public function __construct(Translator $translator)
@@ -36,11 +36,11 @@ class LocaleMiddleware implements MiddlewareInterface
*/
public function process(Request $request, DelegateInterface $delegate): Response
{
if (! $request->hasHeader('Accept-Language')) {
if (! $request->hasHeader(self::ACCEPT_LANGUAGE)) {
return $delegate->handle($request);
}
$locale = $request->getHeaderLine('Accept-Language');
$locale = $request->getHeaderLine(self::ACCEPT_LANGUAGE);
$this->translator->setLocale($this->normalizeLocale($locale));
return $delegate->handle($request);
}

View File

@@ -12,21 +12,13 @@ class PaginableRepositoryAdapter implements AdapterInterface
{
public const ITEMS_PER_PAGE = 10;
/**
* @var PaginableRepositoryInterface
*/
/** @var PaginableRepositoryInterface */
private $paginableRepository;
/**
* @var null|string
*/
/** @var null|string */
private $searchTerm;
/**
* @var null|array|string
*/
/** @var null|array|string */
private $orderBy;
/**
* @var array
*/
/** @var array */
private $tags;
public function __construct(

View File

@@ -12,17 +12,11 @@ use function urlencode;
class PreviewGenerator implements PreviewGeneratorInterface
{
/**
* @var string
*/
/** @var string */
private $location;
/**
* @var ImageBuilderInterface
*/
/** @var ImageBuilderInterface */
private $imageBuilder;
/**
* @var Filesystem
*/
/** @var Filesystem */
private $filesystem;
public function __construct(ImageBuilderInterface $imageBuilder, Filesystem $filesystem, $location)

View File

@@ -9,9 +9,7 @@ use Zend\I18n\Translator\TranslatorInterface;
class TranslatorExtension implements ExtensionInterface
{
/**
* @var TranslatorInterface
*/
/** @var TranslatorInterface */
private $translator;
public function __construct(TranslatorInterface $translator)
@@ -19,9 +17,8 @@ class TranslatorExtension implements ExtensionInterface
$this->translator = $translator;
}
public function register(Engine $engine)
public function register(Engine $engine): void
{
$engine->registerFunction('translate', [$this->translator, 'translate']);
$engine->registerFunction('translate_plural', [$this->translator, 'translatePlural']);
}
}

View File

@@ -7,13 +7,9 @@ use Cake\Chronos\Chronos;
final class DateRange
{
/**
* @var Chronos|null
*/
/** @var Chronos|null */
private $startDate;
/**
* @var Chronos|null
*/
/** @var Chronos|null */
private $endDate;
public function __construct(?Chronos $startDate = null, ?Chronos $endDate = null)

View File

@@ -15,21 +15,13 @@ final class IpAddress
private const OBFUSCATED_OCTET = '0';
public const LOCALHOST = '127.0.0.1';
/**
* @var string
*/
/** @var string */
private $firstOctet;
/**
* @var string
*/
/** @var string */
private $secondOctet;
/**
* @var string
*/
/** @var string */
private $thirdOctet;
/**
* @var string
*/
/** @var string */
private $fourthOctet;
private function __construct(string $firstOctet, string $secondOctet, string $thirdOctet, string $fourthOctet)

View File

@@ -10,9 +10,7 @@ abstract class DatabaseTestCase extends TestCase
{
protected const ENTITIES_TO_EMPTY = [];
/**
* @var EntityManagerInterface
*/
/** @var EntityManagerInterface */
public static $em;
protected function getEntityManager(): EntityManagerInterface

View File

@@ -8,9 +8,7 @@ use Shlinkio\Shlink\Common\Collection\PathCollection;
class PathCollectionTest extends TestCase
{
/**
* @var PathCollection
*/
/** @var PathCollection */
private $collection;
public function setUp()

View File

@@ -8,10 +8,8 @@ use Shlinkio\Shlink\Common\ConfigProvider;
class ConfigProviderTest extends TestCase
{
/**
* @var ConfigProvider
*/
protected $configProvider;
/** @var ConfigProvider */
private $configProvider;
public function setUp()
{

View File

@@ -19,10 +19,8 @@ use function sys_get_temp_dir;
class CacheFactoryTest extends TestCase
{
/**
* @var CacheFactory
*/
protected $factory;
/** @var CacheFactory */
private $factory;
public function setUp()
{

View File

@@ -11,9 +11,7 @@ use Zend\ServiceManager\ServiceManager;
class DottedAccessConfigAbstractFactoryTest extends TestCase
{
/**
* @var DottedAccessConfigAbstractFactory
*/
/** @var DottedAccessConfigAbstractFactory */
private $factory;
public function setUp()

View File

@@ -12,10 +12,8 @@ use Zend\ServiceManager\ServiceManager;
class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase
{
/**
* @var EmptyResponseImplicitOptionsMiddlewareFactory
*/
protected $factory;
/** @var EmptyResponseImplicitOptionsMiddlewareFactory */
private $factory;
public function setUp()
{

View File

@@ -10,10 +10,8 @@ use Zend\ServiceManager\ServiceManager;
class EntityManagerFactoryTest extends TestCase
{
/**
* @var EntityManagerFactory
*/
protected $factory;
/** @var EntityManagerFactory */
private $factory;
public function setUp()
{

View File

@@ -11,10 +11,8 @@ use Zend\ServiceManager\ServiceManager;
class LoggerFactoryTest extends TestCase
{
/**
* @var LoggerFactory
*/
protected $factory;
/** @var LoggerFactory */
private $factory;
public function setUp()
{

View File

@@ -10,10 +10,8 @@ use Zend\ServiceManager\ServiceManager;
class TranslatorFactoryTest extends TestCase
{
/**
* @var TranslatorFactory
*/
protected $factory;
/** @var TranslatorFactory */
private $factory;
public function setUp()
{

View File

@@ -10,10 +10,8 @@ use Zend\ServiceManager\ServiceManager;
class ImageBuilderFactoryTest extends TestCase
{
/**
* @var ImageBuilderFactory
*/
protected $factory;
/** @var ImageBuilderFactory */
private $factory;
public function setUp()
{

View File

@@ -11,10 +11,8 @@ use Zend\ServiceManager\ServiceManager;
class ImageFactoryTest extends TestCase
{
/**
* @var ImageFactory
*/
protected $factory;
/** @var ImageFactory */
private $factory;
public function setUp()
{

View File

@@ -11,17 +11,11 @@ use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
class ChainIpLocationResolverTest extends TestCase
{
/**
* @var ChainIpLocationResolver
*/
/** @var ChainIpLocationResolver */
private $resolver;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $firstInnerResolver;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $secondInnerResolver;
public function setUp()

View File

@@ -23,9 +23,7 @@ class EmptyIpLocationResolverTest extends TestCase
'time_zone' => '',
];
/**
* @var EmptyIpLocationResolver
*/
/** @var EmptyIpLocationResolver */
private $resolver;
public function setUp()

View File

@@ -17,21 +17,13 @@ use Zend\Diactoros\Response;
class DbUpdaterTest extends TestCase
{
/**
* @var DbUpdater
*/
/** @var DbUpdater */
private $dbUpdater;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $httpClient;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $filesystem;
/**
* @var GeoLite2Options
*/
/** @var GeoLite2Options */
private $options;
public function setUp()

View File

@@ -14,13 +14,9 @@ use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2LocationResolver;
class GeoLite2LocationResolverTest extends TestCase
{
/**
* @var GeoLite2LocationResolver
*/
/** @var GeoLite2LocationResolver */
private $resolver;
/**
* @var ObjectProphecy
*/
/** @var ObjectProphecy */
private $reader;
public function setUp()

View File

@@ -13,14 +13,10 @@ use function json_encode;
class IpApiLocationResolverTest extends TestCase
{
/**
* @var IpApiLocationResolver
*/
protected $ipResolver;
/**
* @var ObjectProphecy
*/
protected $client;
/** @var IpApiLocationResolver */
private $ipResolver;
/** @var ObjectProphecy */
private $client;
public function setUp()
{

View File

@@ -9,9 +9,7 @@ use const PHP_EOL;
class ExceptionWithNewLineProcessorTest extends TestCase
{
/**
* @var ExceptionWithNewLineProcessor
*/
/** @var ExceptionWithNewLineProcessor */
private $processor;
public function setUp()

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Logger\Swoole;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use ReflectionObject;
use Shlinkio\Shlink\Common\Logger\Swoole\AccessLogFactory;
use Zend\Expressive\Swoole\Log\AccessLogFormatter;
use Zend\Expressive\Swoole\Log\AccessLogFormatterInterface;
use Zend\Expressive\Swoole\Log\Psr3AccessLogDecorator;
use Zend\Expressive\Swoole\Log\StdoutLogger;
use Zend\ServiceManager\ServiceManager;
use function is_string;
class AccessLogFactoryTest extends TestCase
{
/** @var AccessLogFactory */
private $factory;
public function setUp()
{
$this->factory = new AccessLogFactory();
}
/**
* @test
*/
public function createsService()
{
$service = ($this->factory)(new ServiceManager(), '');
$this->assertInstanceOf(Psr3AccessLogDecorator::class, $service);
}
/**
* @test
* @dataProvider provideLoggers
* @param array $config
* @param string|LoggerInterface $expectedLogger
*/
public function wrapsProperLogger(array $config, $expectedLogger)
{
$service = ($this->factory)(new ServiceManager(['services' => $config]), '');
$ref = new ReflectionObject($service);
$loggerProp = $ref->getProperty('logger');
$loggerProp->setAccessible(true);
$logger = $loggerProp->getValue($service);
if (is_string($expectedLogger)) {
$this->assertInstanceOf($expectedLogger, $logger);
} else {
$this->assertSame($expectedLogger, $logger);
}
}
public function provideLoggers(): iterable
{
yield 'without-any-logger' => [[], StdoutLogger::class];
yield 'with-standard-logger' => (function () {
$logger = new NullLogger();
return [[LoggerInterface::class => $logger], $logger];
})();
yield 'with-custom-logger' => (function () {
$logger = new NullLogger();
return [[
'config' => [
'zend-expressive-swoole' => [
'swoole-http-server' => [
'logger' => [
'logger_name' => 'my-logger',
],
],
],
],
'my-logger' => $logger,
], $logger];
})();
}
/**
* @test
* @dataProvider provideFormatters
* @param array $config
* @param string|AccessLogFormatterInterface $expectedFormatter
*/
public function wrappsProperFormatter(array $config, $expectedFormatter, string $expectedFormat)
{
$service = ($this->factory)(new ServiceManager(['services' => $config]), '');
$ref = new ReflectionObject($service);
$formatterProp = $ref->getProperty('formatter');
$formatterProp->setAccessible(true);
$formatter = $formatterProp->getValue($service);
$ref = new ReflectionObject($formatter);
$formatProp = $ref->getProperty('format');
$formatProp->setAccessible(true);
$format = $formatProp->getValue($formatter);
if (is_string($expectedFormatter)) {
$this->assertInstanceOf($expectedFormatter, $formatter);
} else {
$this->assertSame($expectedFormatter, $formatter);
}
$this->assertSame($expectedFormat, $format);
}
public function provideFormatters(): iterable
{
yield 'with-registered-formatter-and-default-format' => (function () {
$formatter = new AccessLogFormatter();
return [[AccessLogFormatterInterface::class => $formatter], $formatter, AccessLogFormatter::FORMAT_COMMON];
})();
yield 'with-registered-formatter-and-custom-format' => (function () {
$formatter = new AccessLogFormatter(AccessLogFormatter::FORMAT_AGENT);
return [[AccessLogFormatterInterface::class => $formatter], $formatter, AccessLogFormatter::FORMAT_AGENT];
})();
yield 'with-no-formatter-and-not-configured-format' => [
[],
AccessLogFormatter::class,
AccessLogFormatter::FORMAT_COMMON,
];
yield 'with-no-formatter-and-configured-format' => [[
'config' => [
'zend-expressive-swoole' => [
'swoole-http-server' => [
'logger' => [
'format' => AccessLogFormatter::FORMAT_COMBINED_DEBIAN,
],
],
],
],
], AccessLogFormatter::class, AccessLogFormatter::FORMAT_COMBINED_DEBIAN];
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Middleware;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Common\Middleware\CloseDbConnectionMiddleware;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
class CloseDbConnectionMiddlewareTest extends TestCase
{
/** @var CloseDbConnectionMiddleware */
private $middleware;
/** @var ObjectProphecy */
private $handler;
/** @var ObjectProphecy */
private $em;
public function setUp()
{
$this->handler = $this->prophesize(RequestHandlerInterface::class);
$this->em = $this->prophesize(EntityManagerInterface::class);
$this->middleware = new CloseDbConnectionMiddleware($this->em->reveal());
}
/**
* @test
*/
public function connectionIsClosedWhenMiddlewareIsProcessed()
{
$req = ServerRequestFactory::fromGlobals();
$resp = new Response();
$conn = $this->prophesize(Connection::class);
$closeConn = $conn->close()->will(function () {
});
$getConn = $this->em->getConnection()->willReturn($conn->reveal());
$clear = $this->em->clear()->will(function () {
});
$handle = $this->handler->handle($req)->willReturn($resp);
$result = $this->middleware->process($req, $this->handler->reveal());
$this->assertSame($result, $resp);
$getConn->shouldHaveBeenCalledOnce();
$closeConn->shouldHaveBeenCalledOnce();
$clear->shouldHaveBeenCalledOnce();
$handle->shouldHaveBeenCalledOnce();
}
}

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