mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-05 14:53:12 +08:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c17c4c1319 | ||
|
|
5967dd97c5 | ||
|
|
0c26198b55 | ||
|
|
a304cca3b6 | ||
|
|
564b65c8ca | ||
|
|
9de0cf5c03 | ||
|
|
1349079f59 | ||
|
|
38016b3ba3 | ||
|
|
8db9962282 | ||
|
|
dca3fb35c7 | ||
|
|
8484449d66 | ||
|
|
6b8ca3e611 | ||
|
|
73fd348490 | ||
|
|
04389fc8b0 | ||
|
|
b0bb77ca81 | ||
|
|
22598e75e8 | ||
|
|
0f8dd1effb | ||
|
|
2c4a8543db | ||
|
|
7aa246b550 | ||
|
|
1e294fe1bc | ||
|
|
dcfb12f454 | ||
|
|
685ee51e1f | ||
|
|
8407fee96d | ||
|
|
7c881377a9 | ||
|
|
acf2961f9e | ||
|
|
f5faeb8f68 | ||
|
|
8985a6932f | ||
|
|
c04f0af56f | ||
|
|
1341d4fe57 | ||
|
|
bc3fc59b1e | ||
|
|
e04838eaa2 | ||
|
|
5d5d89afb9 | ||
|
|
749671c230 | ||
|
|
e79c41d753 | ||
|
|
a575f2eced | ||
|
|
1aba77c752 | ||
|
|
b68e262eac | ||
|
|
f78fa58cf1 | ||
|
|
3916b06e7c | ||
|
|
7fa1f1c63c | ||
|
|
7ed85e8916 | ||
|
|
94e1e6a7b6 | ||
|
|
3cba3f7a4b | ||
|
|
bfd2ce782c | ||
|
|
f99053d251 | ||
|
|
bdc93a45b5 | ||
|
|
a771743756 | ||
|
|
aff1df32f2 | ||
|
|
3562afc2bd | ||
|
|
ac08ed7cf9 | ||
|
|
9cb316bdfa | ||
|
|
6682b52159 | ||
|
|
f5878a5e7b | ||
|
|
406de16a0d | ||
|
|
a73a59f184 | ||
|
|
cca667cf46 | ||
|
|
e6a63a9b85 | ||
|
|
22630c7656 | ||
|
|
c9ec3b3b42 | ||
|
|
a6727c5382 | ||
|
|
9fe2111d62 | ||
|
|
173bfbd300 | ||
|
|
999beef349 | ||
|
|
c6fdd8a59f | ||
|
|
0ec7e8c41b | ||
|
|
89e4ed5573 | ||
|
|
4c76df91ce | ||
|
|
a1c7e7d5da | ||
|
|
f28540a53e | ||
|
|
e0e522c3f5 | ||
|
|
37e286df48 | ||
|
|
bc99ee6ebe | ||
|
|
7e8126a421 | ||
|
|
af4ee8f7ec | ||
|
|
af40e8de5c | ||
|
|
d086131630 | ||
|
|
bccc177414 | ||
|
|
0dfadcbb4a | ||
|
|
4380b62715 | ||
|
|
91698034e7 | ||
|
|
014eb2a924 | ||
|
|
96357a57d2 | ||
|
|
c7cfdffaf6 | ||
|
|
46a27a9d0a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ vendor/
|
||||
data/database.sqlite
|
||||
data/shlink-tests.db
|
||||
data/GeoLite2-City.mmdb
|
||||
data/GeoLite2-City.mmdb.*
|
||||
docs/swagger-ui*
|
||||
docker-compose.override.yml
|
||||
.phpunit.result.cache
|
||||
|
||||
66
CHANGELOG.md
66
CHANGELOG.md
@@ -4,6 +4,72 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||
|
||||
## 1.18.0 - 2019-08-08
|
||||
|
||||
#### Added
|
||||
|
||||
* [#411](https://github.com/shlinkio/shlink/issues/411) Added new `meta` property on the `ShortUrl` REST API model.
|
||||
|
||||
These endpoints are affected and include the new property when suitable:
|
||||
|
||||
* `GET /short-urls` - List short URLs.
|
||||
* `GET /short-urls/shorten` - Create a short URL (for integrations).
|
||||
* `GET /short-urls/{shortCode}` - Get one short URL.
|
||||
* `POST /short-urls` - Create short URL.
|
||||
|
||||
The property includes the values `validSince`, `validUntil` and `maxVisits` in a single object. All of them are nullable.
|
||||
|
||||
```json
|
||||
{
|
||||
"validSince": "2016-01-01T00:00:00+02:00",
|
||||
"validUntil": null,
|
||||
"maxVisits": 100
|
||||
}
|
||||
```
|
||||
|
||||
* [#285](https://github.com/shlinkio/shlink/issues/285) Visit location resolution is now done asynchronously but in real time thanks to swoole task management.
|
||||
|
||||
Now, when a short URL is visited, a task is enqueued to locate it. The user is immediately redirected to the long URL, and in the background, the visit is located, making stats to be available a couple of seconds after the visit without the requirement of cronjobs being run constantly.
|
||||
|
||||
Sadly, this feature is not enabled when serving shlink via apache/nginx, where you should still rely on cronjobs.
|
||||
|
||||
* [#384](https://github.com/shlinkio/shlink/issues/384) Improved how remote IP addresses are detected.
|
||||
|
||||
This new set of headers is now also inspected looking for the IP address:
|
||||
|
||||
* CF-Connecting-IP
|
||||
* True-Client-IP
|
||||
* X-Real-IP
|
||||
|
||||
* [#440](https://github.com/shlinkio/shlink/pull/440) Created `db:create` command, which improves how the shlink database is created, with these benefits:
|
||||
|
||||
* It sets up a lock which prevents the command to be run concurrently.
|
||||
* It checks of the database does not exist, and creates it in that case.
|
||||
* It checks if the database tables already exist, exiting gracefully in that case.
|
||||
|
||||
* [#442](https://github.com/shlinkio/shlink/pull/442) Created `db:migrate` command, which improves doctrine's migrations command by generating a lock, preventing it to be run concurrently.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#430](https://github.com/shlinkio/shlink/issues/430) Updated to [shlinkio/php-coding-standard](https://github.com/shlinkio/php-coding-standard) 1.2.2
|
||||
* [#305](https://github.com/shlinkio/shlink/issues/305) Implemented changes which will allow Shlink to be truly clusterizable.
|
||||
* [#262](https://github.com/shlinkio/shlink/issues/262) Increased mutation score to 75%.
|
||||
|
||||
### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#416](https://github.com/shlinkio/shlink/issues/416) Fixed error thrown when trying to locate visits after the GeoLite2 DB is downloaded for the first time.
|
||||
* [#424](https://github.com/shlinkio/shlink/issues/424) Updated wkhtmltoimage to version 0.12.5
|
||||
* [#427](https://github.com/shlinkio/shlink/issues/427) and [#434](https://github.com/shlinkio/shlink/issues/434) Fixed shlink being unusable after a database error on swoole contexts.
|
||||
|
||||
|
||||
## 1.17.0 - 2019-05-13
|
||||
|
||||
#### Added
|
||||
|
||||
@@ -188,7 +188,7 @@ There are a couple of time-consuming tasks that shlink expects you to do manuall
|
||||
|
||||
Those tasks can be performed using shlink's CLI, so it should be easy to schedule them to be run in the background (for example, using cron jobs):
|
||||
|
||||
* Resolve IP address locations: `/path/to/shlink/bin/cli visit:locate`
|
||||
* **For shlink older than 1.18.0 or not using swoole as the web server**: Resolve IP address locations: `/path/to/shlink/bin/cli visit:locate`
|
||||
|
||||
If you don't run this command regularly, the stats will say all visits come from *unknown* locations.
|
||||
|
||||
@@ -204,7 +204,7 @@ Those tasks can be performed using shlink's CLI, so it should be easy to schedul
|
||||
|
||||
*Any of these 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.
|
||||
> 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
|
||||
|
||||
@@ -268,6 +268,9 @@ Available commands:
|
||||
config
|
||||
config:generate-charset [DEPRECATED] Generates a character set sample just by shuffling the default one, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ". Then it can be set in the SHORTCODE_CHARS environment variable
|
||||
config:generate-secret [DEPRECATED] Generates a random secret string that can be used for JWT token encryption
|
||||
db
|
||||
db:create Creates the database needed for shlink to work. It will do nothing if the database already exists
|
||||
db:migrate Runs database migrations, which will ensure the shlink database is up to date.
|
||||
short-url
|
||||
short-url:delete [short-code:delete] Deletes a short URL
|
||||
short-url:generate [shortcode:generate|short-code:generate] Generates a short URL for provided long URL and returns it
|
||||
|
||||
Binary file not shown.
9
build.sh
9
build.sh
@@ -20,8 +20,14 @@ rsync -av * "${builtcontent}" \
|
||||
--exclude=bin/test \
|
||||
--exclude=data/infra \
|
||||
--exclude=data/travis \
|
||||
--exclude=data/cache/* \
|
||||
--exclude=data/log/* \
|
||||
--exclude=data/locks/* \
|
||||
--exclude=data/proxies/* \
|
||||
--exclude=data/migrations_template.txt \
|
||||
--exclude=data/GeoLite2-City.mmdb \
|
||||
--exclude=data/GeoLite2-City.* \
|
||||
--exclude=data/database.sqlite \
|
||||
--exclude=data/shlink-tests.db \
|
||||
--exclude=**/.gitignore \
|
||||
--exclude=CHANGELOG.md \
|
||||
--exclude=composer.lock \
|
||||
@@ -47,7 +53,6 @@ ${composerBin} install --no-dev --optimize-autoloader --apcu-autoloader --no-pro
|
||||
# Delete development files
|
||||
echo 'Deleting dev files...'
|
||||
rm composer.*
|
||||
rm -f data/database.sqlite
|
||||
|
||||
# Update shlink version in config
|
||||
sed -i "s/%SHLINK_VERSION%/${version}/g" config/autoload/app_options.global.php
|
||||
|
||||
@@ -29,50 +29,56 @@
|
||||
"lstrojny/functional-php": "^1.8",
|
||||
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
||||
"monolog/monolog": "^1.21",
|
||||
"shlinkio/shlink-installer": "^1.1",
|
||||
"symfony/console": "^4.2",
|
||||
"symfony/filesystem": "^4.2",
|
||||
"symfony/lock": "^4.2",
|
||||
"symfony/process": "^4.2",
|
||||
"ocramius/proxy-manager": "~2.2.2",
|
||||
"phly/phly-event-dispatcher": "^1.0",
|
||||
"predis/predis": "^1.1",
|
||||
"shlinkio/shlink-installer": "^1.2.1",
|
||||
"symfony/console": "^4.3",
|
||||
"symfony/filesystem": "^4.3",
|
||||
"symfony/lock": "^4.3",
|
||||
"symfony/process": "^4.3",
|
||||
"theorchard/monolog-cascade": "^0.4",
|
||||
"zendframework/zend-config": "^3.0",
|
||||
"zendframework/zend-config-aggregator": "^1.0",
|
||||
"zendframework/zend-diactoros": "^2.1.1",
|
||||
"zendframework/zend-expressive": "^3.0",
|
||||
"zendframework/zend-config": "^3.3",
|
||||
"zendframework/zend-config-aggregator": "^1.1",
|
||||
"zendframework/zend-diactoros": "^2.1.3",
|
||||
"zendframework/zend-expressive": "^3.2",
|
||||
"zendframework/zend-expressive-fastroute": "^3.0",
|
||||
"zendframework/zend-expressive-helpers": "^5.0",
|
||||
"zendframework/zend-expressive-platesrenderer": "^2.0",
|
||||
"zendframework/zend-expressive-helpers": "^5.3",
|
||||
"zendframework/zend-expressive-platesrenderer": "^2.1",
|
||||
"zendframework/zend-expressive-swoole": "^2.4",
|
||||
"zendframework/zend-i18n": "^2.7",
|
||||
"zendframework/zend-inputfilter": "^2.8",
|
||||
"zendframework/zend-paginator": "^2.6",
|
||||
"zendframework/zend-servicemanager": "^3.2",
|
||||
"zendframework/zend-stdlib": "^3.0"
|
||||
"zendframework/zend-i18n": "^2.9",
|
||||
"zendframework/zend-inputfilter": "^2.10",
|
||||
"zendframework/zend-paginator": "^2.8",
|
||||
"zendframework/zend-servicemanager": "^3.4",
|
||||
"zendframework/zend-stdlib": "^3.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"devster/ubench": "^2.0",
|
||||
"doctrine/data-fixtures": "^1.3",
|
||||
"filp/whoops": "^2.0",
|
||||
"eaglewu/swoole-ide-helper": "dev-master",
|
||||
"filp/whoops": "^2.4",
|
||||
"infection/infection": "^0.12.2",
|
||||
"phpstan/phpstan": "^0.11.2",
|
||||
"phpunit/phpcov": "^6.0",
|
||||
"phpunit/phpunit": "^8.0",
|
||||
"phpunit/phpunit": "^8.3",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~1.1.0",
|
||||
"symfony/dotenv": "^4.2",
|
||||
"symfony/var-dumper": "^4.2",
|
||||
"shlinkio/php-coding-standard": "~1.2.2",
|
||||
"symfony/dotenv": "^4.3",
|
||||
"symfony/var-dumper": "^4.3",
|
||||
"zendframework/zend-component-installer": "^2.1",
|
||||
"zendframework/zend-expressive-tooling": "^1.0"
|
||||
"zendframework/zend-expressive-tooling": "^1.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
|
||||
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
|
||||
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
|
||||
"Shlinkio\\Shlink\\Common\\": "module/Common/src"
|
||||
"Shlinkio\\Shlink\\Common\\": "module/Common/src",
|
||||
"Shlinkio\\Shlink\\EventDispatcher\\": "module/EventDispatcher/src"
|
||||
},
|
||||
"files": [
|
||||
"module/Common/functions/functions.php"
|
||||
"module/Common/functions/functions.php",
|
||||
"module/EventDispatcher/functions/functions.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
@@ -87,7 +93,8 @@
|
||||
"ShlinkioTest\\Shlink\\Common\\": [
|
||||
"module/Common/test",
|
||||
"module/Common/test-db"
|
||||
]
|
||||
],
|
||||
"ShlinkioTest\\Shlink\\EventDispatcher\\": "module/EventDispatcher/test"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -110,15 +117,18 @@
|
||||
"test:ci": [
|
||||
"@test:unit:ci",
|
||||
"@test:db",
|
||||
"@test:db:mysql",
|
||||
"@test:db:postgres",
|
||||
"@test:api"
|
||||
],
|
||||
"test:unit": "phpdbg -qrr vendor/bin/phpunit --order-by=random --coverage-php build/coverage-unit.cov --testdox",
|
||||
"test:unit:ci": "phpdbg -qrr vendor/bin/phpunit --order-by=random --coverage-php build/coverage-unit.cov --coverage-clover=build/clover.xml --coverage-xml=build/coverage-xml --log-junit=build/phpunit.junit.xml --testdox",
|
||||
"test:db": "APP_ENV=test phpdbg -qrr vendor/bin/phpunit --order-by=random -c phpunit-db.xml --coverage-php build/coverage-db.cov --testdox",
|
||||
"test:db:mysql": "DB_DRIVER=mysql composer test:db",
|
||||
"test:db:postgres": "DB_DRIVER=postgres composer test:db",
|
||||
"test:db": [
|
||||
"@test:db:sqlite",
|
||||
"@test:db:mysql",
|
||||
"@test:db:postgres"
|
||||
],
|
||||
"test:db:sqlite": "APP_ENV=test phpdbg -qrr vendor/bin/phpunit --order-by=random -c phpunit-db.xml --coverage-php build/coverage-db.cov --testdox",
|
||||
"test:db:mysql": "DB_DRIVER=mysql composer test:db:sqlite",
|
||||
"test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite",
|
||||
"test:api": "bin/test/run-api-tests.sh",
|
||||
|
||||
"test:pretty": [
|
||||
@@ -127,9 +137,9 @@
|
||||
],
|
||||
"test:unit:pretty": "phpdbg -qrr vendor/bin/phpunit --coverage-html build/coverage --order-by=random",
|
||||
|
||||
"infect": "infection --threads=4 --min-msi=70 --log-verbosity=default --only-covered",
|
||||
"infect:ci": "infection --threads=4 --min-msi=70 --log-verbosity=default --only-covered --coverage=build",
|
||||
"infect:show": "infection --threads=4 --min-msi=70 --log-verbosity=default --only-covered --show-mutations",
|
||||
"infect": "infection --threads=4 --min-msi=75 --log-verbosity=default --only-covered",
|
||||
"infect:ci": "infection --threads=4 --min-msi=75 --log-verbosity=default --only-covered --coverage=build",
|
||||
"infect:show": "infection --threads=4 --min-msi=75 --log-verbosity=default --only-covered --show-mutations",
|
||||
"infect:test": [
|
||||
"@test:unit:ci",
|
||||
"@infect:ci"
|
||||
@@ -145,7 +155,8 @@
|
||||
"test:ci": "<fg=blue;options=bold>Runs all test suites, generating all needed reports and logs for CI envs</>",
|
||||
"test:unit": "<fg=blue;options=bold>Runs unit test suites</>",
|
||||
"test:unit:ci": "<fg=blue;options=bold>Runs unit test suites, generating all needed reports and logs for CI envs</>",
|
||||
"test:db": "<fg=blue;options=bold>Runs database test suites on a SQLite database</>",
|
||||
"test:db": "<fg=blue;options=bold>Runs database test suites on a SQLite, MySQL and PostgreSQL</>",
|
||||
"test:db:sqlite": "<fg=blue;options=bold>Runs database test suites on a SQLite database</>",
|
||||
"test:db:mysql": "<fg=blue;options=bold>Runs database test suites on a MySQL database</>",
|
||||
"test:db:postgres": "<fg=blue;options=bold>Runs database test suites on a PostgreSQL database</>",
|
||||
"test:api": "<fg=blue;options=bold>Runs API test suites</>",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
namespace Shlinkio\Shlink\Common;
|
||||
|
||||
return [
|
||||
|
||||
|
||||
@@ -36,4 +36,13 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'installation_commands' => [
|
||||
'db_create_schema' => [
|
||||
'command' => 'bin/cli db:create',
|
||||
],
|
||||
'db_migrate' => [
|
||||
'command' => 'bin/cli db:migrate',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
19
config/autoload/ip-address.global.php
Normal file
19
config/autoload/ip-address.global.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'ip_address_resolution' => [
|
||||
'headers_to_inspect' => [
|
||||
'CF-Connecting-IP',
|
||||
'True-Client-IP',
|
||||
'X-Real-IP',
|
||||
'Forwarded',
|
||||
'X-Forwarded-For',
|
||||
'X-Forwarded',
|
||||
'X-Cluster-Client-Ip',
|
||||
'Client-Ip',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Common\Cache\RedisFactory;
|
||||
use Shlinkio\Shlink\Common\Lock\RetryLockStoreDelegatorFactory;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory;
|
||||
use Symfony\Component\Lock;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
|
||||
@@ -13,13 +16,28 @@ return [
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Lock\Store\FlockStore::class => ConfigAbstractFactory::class,
|
||||
Lock\Store\RedisStore::class => ConfigAbstractFactory::class,
|
||||
Lock\Factory::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
// With this config, a user could alias 'lock_store' => 'redis_lock_store' to override the default
|
||||
'lock_store' => Lock\Store\FlockStore::class,
|
||||
'redis_lock_store' => Lock\Store\RedisStore::class,
|
||||
],
|
||||
'delegators' => [
|
||||
Lock\Store\RedisStore::class => [
|
||||
RetryLockStoreDelegatorFactory::class,
|
||||
],
|
||||
Lock\Factory::class => [
|
||||
LoggerAwareDelegatorFactory::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Lock\Store\FlockStore::class => ['config.locks.locks_dir'],
|
||||
Lock\Factory::class => [Lock\Store\FlockStore::class],
|
||||
Lock\Store\RedisStore::class => [RedisFactory::SERVICE_NAME],
|
||||
Lock\Factory::class => ['lock_store'],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -58,8 +58,8 @@ return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
'Logger_Shlink' => Common\Factory\LoggerFactory::class,
|
||||
'Logger_Access' => Common\Factory\LoggerFactory::class,
|
||||
'Logger_Shlink' => Common\Logger\LoggerFactory::class,
|
||||
'Logger_Access' => Common\Logger\LoggerFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -1,16 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
|
||||
return [
|
||||
$isSwoole = extension_loaded('swoole');
|
||||
|
||||
'logger' => [
|
||||
'handlers' => [
|
||||
'shlink_rotating_handler' => [
|
||||
'level' => Logger::DEBUG,
|
||||
],
|
||||
// For swoole, send logs to standard output
|
||||
$logger = $isSwoole ? [
|
||||
'handlers' => [
|
||||
'shlink_rotating_handler' => [
|
||||
'level' => Logger::EMERGENCY, // This basically disables regular file logs
|
||||
],
|
||||
'shlink_stdout_handler' => [
|
||||
'class' => StreamHandler::class,
|
||||
'level' => Logger::DEBUG,
|
||||
'stream' => 'php://stdout',
|
||||
'formatter' => 'dashed',
|
||||
],
|
||||
],
|
||||
|
||||
'loggers' => [
|
||||
'Shlink' => [
|
||||
'handlers' => ['shlink_stdout_handler'],
|
||||
],
|
||||
],
|
||||
] : [
|
||||
'handlers' => [
|
||||
'shlink_rotating_handler' => [
|
||||
'level' => Logger::DEBUG,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return [
|
||||
|
||||
'logger' => $logger,
|
||||
|
||||
];
|
||||
|
||||
20
config/autoload/redis.local.php.local
Normal file
20
config/autoload/redis.local.php.local
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'redis' => [
|
||||
'servers' => 'tcp://shlink_redis:6379',
|
||||
// 'servers' => [
|
||||
// 'tcp://shlink_redis:6379',
|
||||
// ],
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'aliases' => [
|
||||
// With this config, a user could alias 'lock_store' => 'redis_lock_store' to override the default
|
||||
// 'lock_store' => 'redis_lock_store',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -9,6 +9,11 @@ return [
|
||||
'swoole-http-server' => [
|
||||
'host' => '0.0.0.0',
|
||||
'process-name' => 'shlink',
|
||||
|
||||
'options' => [
|
||||
'worker_num' => 16,
|
||||
'task_worker_num' => 16,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ return (new ConfigAggregator\ConfigAggregator([
|
||||
Core\ConfigProvider::class,
|
||||
CLI\ConfigProvider::class,
|
||||
Rest\ConfigProvider::class,
|
||||
EventDispatcher\ConfigProvider::class,
|
||||
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
|
||||
new ConfigAggregator\ZendConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'),
|
||||
env('APP_ENV') === 'test'
|
||||
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
: new ConfigAggregator\ZendConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'),
|
||||
], 'data/cache/app_config.php', [
|
||||
Core\ConfigPostProcessor::class,
|
||||
Core\SimplifiedConfigParser::class,
|
||||
]))->getMergedConfig();
|
||||
|
||||
@@ -70,6 +70,8 @@ return [
|
||||
'process-name' => 'shlink_test',
|
||||
'options' => [
|
||||
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
|
||||
'worker_num' => 1,
|
||||
'task_worker_num' => 1,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -84,7 +84,9 @@ WORKDIR /home/shlink
|
||||
# Expose swoole port
|
||||
EXPOSE 8080
|
||||
|
||||
CMD /usr/local/bin/composer update && \
|
||||
CMD \
|
||||
# Install dependencies if the vendor dir does not exist
|
||||
if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \
|
||||
# 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
|
||||
|
||||
@@ -24,6 +24,7 @@ services:
|
||||
links:
|
||||
- shlink_db
|
||||
- shlink_db_postgres
|
||||
- shlink_redis
|
||||
|
||||
shlink_swoole:
|
||||
container_name: shlink_swoole
|
||||
@@ -37,6 +38,7 @@ services:
|
||||
links:
|
||||
- shlink_db
|
||||
- shlink_db_postgres
|
||||
- shlink_redis
|
||||
|
||||
shlink_db:
|
||||
container_name: shlink_db
|
||||
@@ -62,3 +64,9 @@ services:
|
||||
POSTGRES_PASSWORD: root
|
||||
POSTGRES_DB: shlink
|
||||
PGDATA: /var/lib/postgresql/data/pgdata
|
||||
|
||||
shlink_redis:
|
||||
container_name: shlink_redis
|
||||
image: redis:5.0-alpine
|
||||
ports:
|
||||
- "6380:6379"
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
},
|
||||
"description": "A list of tags applied to this short URL"
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "./ShortUrlMeta.json"
|
||||
},
|
||||
"originalUrl": {
|
||||
"deprecated": true,
|
||||
"type": "string",
|
||||
|
||||
21
docs/swagger/definitions/ShortUrlMeta.json
Normal file
21
docs/swagger/definitions/ShortUrlMeta.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["validSince", "validUntil", "maxVisits"],
|
||||
"properties": {
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"validUntil": {
|
||||
"description": "The date (in ISO-8601 format) until which this short code will be valid",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"maxVisits": {
|
||||
"description": "The maximum number of allowed visits for this short code",
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,12 @@
|
||||
"tags": [
|
||||
"games",
|
||||
"tech"
|
||||
]
|
||||
],
|
||||
"meta": {
|
||||
"validSince": "2017-01-21T00:00:00+02:00",
|
||||
"validUntil": null,
|
||||
"maxVisits": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"shortCode": "12Kb3",
|
||||
@@ -110,7 +115,12 @@
|
||||
"visitsCount": 1029,
|
||||
"tags": [
|
||||
"shlink"
|
||||
]
|
||||
],
|
||||
"meta": {
|
||||
"validSince": null,
|
||||
"validUntil": null,
|
||||
"maxVisits": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"shortCode": "123bA",
|
||||
@@ -118,7 +128,12 @@
|
||||
"longUrl": "https://www.google.com",
|
||||
"dateCreated": "2015-10-01T20:34:16+02:00",
|
||||
"visitsCount": 25,
|
||||
"tags": []
|
||||
"tags": [],
|
||||
"meta": {
|
||||
"validSince": "2017-01-21T00:00:00+02:00",
|
||||
"validUntil": null,
|
||||
"maxVisits": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
@@ -227,7 +242,12 @@
|
||||
"tags": [
|
||||
"games",
|
||||
"tech"
|
||||
]
|
||||
],
|
||||
"meta": {
|
||||
"validSince": "2017-01-21T00:00:00+02:00",
|
||||
"validUntil": null,
|
||||
"maxVisits": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -64,7 +64,12 @@
|
||||
"tags": [
|
||||
"games",
|
||||
"tech"
|
||||
]
|
||||
],
|
||||
"meta": {
|
||||
"validSince": "2017-01-21T00:00:00+02:00",
|
||||
"validUntil": null,
|
||||
"maxVisits": 100
|
||||
}
|
||||
},
|
||||
"text/plain": "https://doma.in/abc123"
|
||||
}
|
||||
|
||||
@@ -44,7 +44,12 @@
|
||||
"visitsCount": 1029,
|
||||
"tags": [
|
||||
"shlink"
|
||||
]
|
||||
],
|
||||
"meta": {
|
||||
"validSince": "2017-01-21T00:00:00+02:00",
|
||||
"validUntil": null,
|
||||
"maxVisits": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -28,6 +28,9 @@ return [
|
||||
Command\Tag\CreateTagCommand::NAME => Command\Tag\CreateTagCommand::class,
|
||||
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
||||
Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::class,
|
||||
|
||||
Command\Db\CreateDatabaseCommand::NAME => Command\Db\CreateDatabaseCommand::class,
|
||||
Command\Db\MigrateDatabaseCommand::NAME => Command\Db\MigrateDatabaseCommand::class,
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -3,15 +3,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use GeoIp2\Database\Reader;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdater;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Core\Service;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Lock;
|
||||
use Symfony\Component\Console as SymfonyCli;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
@@ -19,7 +22,9 @@ return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Application::class => Factory\ApplicationFactory::class,
|
||||
SymfonyCli\Application::class => Factory\ApplicationFactory::class,
|
||||
SymfonyCli\Helper\ProcessHelper::class => Factory\ProcessHelperFactory::class,
|
||||
PhpExecutableFinder::class => InvokableFactory::class,
|
||||
|
||||
GeolocationDbUpdater::class => ConfigAbstractFactory::class,
|
||||
|
||||
@@ -44,11 +49,14 @@ return [
|
||||
Command\Tag\CreateTagCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Tag\DeleteTagsCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Db\CreateDatabaseCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Db\MigrateDatabaseCommand::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
GeolocationDbUpdater::class => [DbUpdater::class, Reader::class],
|
||||
GeolocationDbUpdater::class => [DbUpdater::class, Reader::class, Locker::class],
|
||||
|
||||
Command\ShortUrl\GenerateShortUrlCommand::class => [Service\UrlShortener::class, 'config.url_shortener.domain'],
|
||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class],
|
||||
@@ -60,7 +68,7 @@ return [
|
||||
Command\Visit\LocateVisitsCommand::class => [
|
||||
Service\VisitService::class,
|
||||
IpLocationResolverInterface::class,
|
||||
Lock\Factory::class,
|
||||
Locker::class,
|
||||
GeolocationDbUpdater::class,
|
||||
],
|
||||
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
|
||||
@@ -73,6 +81,19 @@ return [
|
||||
Command\Tag\CreateTagCommand::class => [Service\Tag\TagService::class],
|
||||
Command\Tag\RenameTagCommand::class => [Service\Tag\TagService::class],
|
||||
Command\Tag\DeleteTagsCommand::class => [Service\Tag\TagService::class],
|
||||
|
||||
Command\Db\CreateDatabaseCommand::class => [
|
||||
Locker::class,
|
||||
SymfonyCli\Helper\ProcessHelper::class,
|
||||
PhpExecutableFinder::class,
|
||||
Connection::class,
|
||||
NoDbNameConnectionFactory::SERVICE_NAME,
|
||||
],
|
||||
Command\Db\MigrateDatabaseCommand::class => [
|
||||
Locker::class,
|
||||
SymfonyCli\Helper\ProcessHelper::class,
|
||||
PhpExecutableFinder::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
33
module/CLI/src/Command/Db/AbstractDatabaseCommand.php
Normal file
33
module/CLI/src/Command/Db/AbstractDatabaseCommand.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Db;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
use function array_unshift;
|
||||
|
||||
abstract class AbstractDatabaseCommand extends AbstractLockedCommand
|
||||
{
|
||||
/** @var ProcessHelper */
|
||||
private $processHelper;
|
||||
/** @var string */
|
||||
private $phpBinary;
|
||||
|
||||
public function __construct(Locker $locker, ProcessHelper $processHelper, PhpExecutableFinder $phpFinder)
|
||||
{
|
||||
parent::__construct($locker);
|
||||
$this->processHelper = $processHelper;
|
||||
$this->phpBinary = $phpFinder->find(false) ?: 'php';
|
||||
}
|
||||
|
||||
protected function runPhpCommand(OutputInterface $output, array $command): void
|
||||
{
|
||||
array_unshift($command, $this->phpBinary);
|
||||
$this->processHelper->run($output, $command, null, null, $output->getVerbosity());
|
||||
}
|
||||
}
|
||||
98
module/CLI/src/Command/Db/CreateDatabaseCommand.php
Normal file
98
module/CLI/src/Command/Db/CreateDatabaseCommand.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Db;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
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 Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
use function Functional\contains;
|
||||
|
||||
class CreateDatabaseCommand extends AbstractDatabaseCommand
|
||||
{
|
||||
public const NAME = 'db:create';
|
||||
public const DOCTRINE_HELPER_SCRIPT = 'vendor/doctrine/orm/bin/doctrine.php';
|
||||
public const DOCTRINE_HELPER_COMMAND = 'orm:schema-tool:create';
|
||||
|
||||
/** @var Connection */
|
||||
private $regularConn;
|
||||
/** @var Connection */
|
||||
private $noDbNameConn;
|
||||
|
||||
public function __construct(
|
||||
Locker $locker,
|
||||
ProcessHelper $processHelper,
|
||||
PhpExecutableFinder $phpFinder,
|
||||
Connection $conn,
|
||||
Connection $noDbNameConn
|
||||
) {
|
||||
parent::__construct($locker, $processHelper, $phpFinder);
|
||||
$this->regularConn = $conn;
|
||||
$this->noDbNameConn = $noDbNameConn;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription(
|
||||
'Creates the database needed for shlink to work. It will do nothing if the database already exists'
|
||||
);
|
||||
}
|
||||
|
||||
protected function lockedExecute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$this->checkDbExists();
|
||||
|
||||
if ($this->schemaExists()) {
|
||||
$io->success('Database already exists. Run "db:migrate" command to make sure it is up to date.');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
// Create database
|
||||
$io->writeln('<fg=blue>Creating database tables...</>');
|
||||
$this->runPhpCommand($output, [self::DOCTRINE_HELPER_SCRIPT, self::DOCTRINE_HELPER_COMMAND]);
|
||||
$io->success('Database properly created!');
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function checkDbExists(): void
|
||||
{
|
||||
if ($this->regularConn->getDatabasePlatform()->getName() === 'sqlite') {
|
||||
return;
|
||||
}
|
||||
|
||||
// In order to create the new database, we have to use a connection where the dbname was not set.
|
||||
// Otherwise, it will fail to connect and will not be able to create the new database
|
||||
$schemaManager = $this->noDbNameConn->getSchemaManager();
|
||||
$databases = $schemaManager->listDatabases();
|
||||
$shlinkDatabase = $this->regularConn->getDatabase();
|
||||
|
||||
if (! contains($databases, $shlinkDatabase)) {
|
||||
$schemaManager->createDatabase($shlinkDatabase);
|
||||
}
|
||||
}
|
||||
|
||||
private function schemaExists(): bool
|
||||
{
|
||||
// If at least one of the shlink tables exist, we will consider the database exists somehow.
|
||||
// Any inconsistency will be taken care by the migrations
|
||||
$schemaManager = $this->regularConn->getSchemaManager();
|
||||
return ! empty($schemaManager->listTableNames());
|
||||
}
|
||||
|
||||
protected function getLockConfig(): LockedCommandConfig
|
||||
{
|
||||
return new LockedCommandConfig($this->getName(), true);
|
||||
}
|
||||
}
|
||||
40
module/CLI/src/Command/Db/MigrateDatabaseCommand.php
Normal file
40
module/CLI/src/Command/Db/MigrateDatabaseCommand.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Db;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class MigrateDatabaseCommand extends AbstractDatabaseCommand
|
||||
{
|
||||
public const NAME = 'db:migrate';
|
||||
public const DOCTRINE_HELPER_SCRIPT = 'vendor/doctrine/migrations/bin/doctrine-migrations.php';
|
||||
public const DOCTRINE_HELPER_COMMAND = 'migrations:migrate';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Runs database migrations, which will ensure the shlink database is up to date.');
|
||||
}
|
||||
|
||||
protected function lockedExecute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->writeln('<fg=blue>Migrating database...</>');
|
||||
$this->runPhpCommand($output, [self::DOCTRINE_HELPER_SCRIPT, self::DOCTRINE_HELPER_COMMAND]);
|
||||
$io->success('Database properly migrated!');
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
protected function getLockConfig(): LockedCommandConfig
|
||||
{
|
||||
return new LockedCommandConfig($this->getName(), true);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
use function array_flip;
|
||||
use function array_intersect_key;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function explode;
|
||||
@@ -29,6 +31,14 @@ class ListShortUrlsCommand extends Command
|
||||
|
||||
public const NAME = 'short-url:list';
|
||||
private const ALIASES = ['shortcode:list', 'short-code:list'];
|
||||
private const COLUMNS_WHITELIST = [
|
||||
'shortCode',
|
||||
'shortUrl',
|
||||
'longUrl',
|
||||
'dateCreated',
|
||||
'visitsCount',
|
||||
'tags',
|
||||
];
|
||||
|
||||
/** @var ShortUrlServiceInterface */
|
||||
private $shortUrlService;
|
||||
@@ -125,8 +135,7 @@ class ListShortUrlsCommand extends Command
|
||||
unset($shortUrl['tags']);
|
||||
}
|
||||
|
||||
unset($shortUrl['originalUrl']);
|
||||
$rows[] = array_values($shortUrl);
|
||||
$rows[] = array_values(array_intersect_key($shortUrl, array_flip(self::COLUMNS_WHITELIST)));
|
||||
}
|
||||
|
||||
ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage(
|
||||
|
||||
47
module/CLI/src/Command/Util/AbstractLockedCommand.php
Normal file
47
module/CLI/src/Command/Util/AbstractLockedCommand.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Util;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
abstract class AbstractLockedCommand extends Command
|
||||
{
|
||||
/** @var Locker */
|
||||
private $locker;
|
||||
|
||||
public function __construct(Locker $locker)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->locker = $locker;
|
||||
}
|
||||
|
||||
final protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$lockConfig = $this->getLockConfig();
|
||||
$lock = $this->locker->createLock($lockConfig->lockName(), $lockConfig->ttl(), $lockConfig->isBlocking());
|
||||
|
||||
if (! $lock->acquire($lockConfig->isBlocking())) {
|
||||
$output->writeln(
|
||||
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName())
|
||||
);
|
||||
return ExitCodes::EXIT_WARNING;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->lockedExecute($input, $output);
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected function lockedExecute(InputInterface $input, OutputInterface $output): int;
|
||||
|
||||
abstract protected function getLockConfig(): LockedCommandConfig;
|
||||
}
|
||||
38
module/CLI/src/Command/Util/LockedCommandConfig.php
Normal file
38
module/CLI/src/Command/Util/LockedCommandConfig.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Util;
|
||||
|
||||
final class LockedCommandConfig
|
||||
{
|
||||
private const DEFAULT_TTL = 90.0; // 1.5 minutes
|
||||
|
||||
/** @var string */
|
||||
private $lockName;
|
||||
/** @var bool */
|
||||
private $isBlocking;
|
||||
/** @var float */
|
||||
private $ttl;
|
||||
|
||||
public function __construct(string $lockName, bool $isBlocking = false, float $ttl = self::DEFAULT_TTL)
|
||||
{
|
||||
$this->lockName = $lockName;
|
||||
$this->isBlocking = $isBlocking;
|
||||
$this->ttl = $ttl;
|
||||
}
|
||||
|
||||
public function lockName(): string
|
||||
{
|
||||
return $this->lockName;
|
||||
}
|
||||
|
||||
public function isBlocking(): bool
|
||||
{
|
||||
return $this->isBlocking;
|
||||
}
|
||||
|
||||
public function ttl(): float
|
||||
{
|
||||
return $this->ttl;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Exception;
|
||||
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||
@@ -14,16 +17,16 @@ use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
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 Symfony\Component\Lock\Factory as Locker;
|
||||
use Throwable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class LocateVisitsCommand extends Command
|
||||
class LocateVisitsCommand extends AbstractLockedCommand
|
||||
{
|
||||
public const NAME = 'visit:locate';
|
||||
public const ALIASES = ['visit:process'];
|
||||
@@ -32,8 +35,6 @@ class LocateVisitsCommand extends Command
|
||||
private $visitService;
|
||||
/** @var IpLocationResolverInterface */
|
||||
private $ipLocationResolver;
|
||||
/** @var Locker */
|
||||
private $locker;
|
||||
/** @var GeolocationDbUpdaterInterface */
|
||||
private $dbUpdater;
|
||||
|
||||
@@ -48,10 +49,9 @@ class LocateVisitsCommand extends Command
|
||||
Locker $locker,
|
||||
GeolocationDbUpdaterInterface $dbUpdater
|
||||
) {
|
||||
parent::__construct();
|
||||
parent::__construct($locker);
|
||||
$this->visitService = $visitService;
|
||||
$this->ipLocationResolver = $ipLocationResolver;
|
||||
$this->locker = $locker;
|
||||
$this->dbUpdater = $dbUpdater;
|
||||
}
|
||||
|
||||
@@ -63,23 +63,17 @@ class LocateVisitsCommand extends Command
|
||||
->setDescription('Resolves visits origin locations.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
protected function lockedExecute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
|
||||
$lock = $this->locker->createLock(self::NAME);
|
||||
if (! $lock->acquire()) {
|
||||
$this->io->warning(sprintf('There is already an instance of the "%s" command in execution', self::NAME));
|
||||
return ExitCodes::EXIT_WARNING;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->checkDbUpdate();
|
||||
|
||||
$this->visitService->locateUnlocatedVisits(
|
||||
[$this, 'getGeolocationDataForVisit'],
|
||||
function (VisitLocation $location) use ($output) {
|
||||
if (! $location->isEmpty()) {
|
||||
static function (VisitLocation $location) use ($output) {
|
||||
if (!$location->isEmpty()) {
|
||||
$output->writeln(
|
||||
sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName())
|
||||
);
|
||||
@@ -88,9 +82,14 @@ class LocateVisitsCommand extends Command
|
||||
);
|
||||
|
||||
$this->io->success('Finished processing all IPs');
|
||||
} finally {
|
||||
$lock->release();
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (Throwable $e) {
|
||||
$this->io->error($e->getMessage());
|
||||
if ($e instanceof Exception && $this->io->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $this->io);
|
||||
}
|
||||
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,4 +151,9 @@ class LocateVisitsCommand extends Command
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getLockConfig(): LockedCommandConfig
|
||||
{
|
||||
return new LockedCommandConfig($this->getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
|
||||
/** @var bool */
|
||||
private $olderDbExists;
|
||||
|
||||
public function __construct(bool $olderDbExists, string $message = '', int $code = 0, Throwable $previous = null)
|
||||
public function __construct(bool $olderDbExists, string $message = '', int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
$this->olderDbExists = $olderDbExists;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public static function create(bool $olderDbExists, Throwable $prev = null): self
|
||||
public static function create(bool $olderDbExists, ?Throwable $prev = null): self
|
||||
{
|
||||
return new self(
|
||||
$olderDbExists,
|
||||
|
||||
@@ -4,32 +4,13 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\CLI\Factory;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
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\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class ApplicationFactory implements FactoryInterface
|
||||
class ApplicationFactory
|
||||
{
|
||||
/**
|
||||
* Create an object
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @param string $requestedName
|
||||
* @param null|array $options
|
||||
* @return CliApp
|
||||
* @throws NotFoundExceptionInterface
|
||||
* @throws ContainerExceptionInterface
|
||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null): CliApp
|
||||
public function __invoke(ContainerInterface $container): CliApp
|
||||
{
|
||||
$config = $container->get('config')['cli'];
|
||||
$appOptions = $container->get(AppOptions::class);
|
||||
|
||||
20
module/CLI/src/Factory/ProcessHelperFactory.php
Normal file
20
module/CLI/src/Factory/ProcessHelperFactory.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Factory;
|
||||
|
||||
use Symfony\Component\Console\Helper;
|
||||
|
||||
class ProcessHelperFactory
|
||||
{
|
||||
public function __invoke(): Helper\ProcessHelper
|
||||
{
|
||||
$processHelper = new Helper\ProcessHelper();
|
||||
$processHelper->setHelperSet(new Helper\HelperSet([
|
||||
new Helper\FormatterHelper(),
|
||||
new Helper\DebugFormatterHelper(),
|
||||
]));
|
||||
|
||||
return $processHelper;
|
||||
}
|
||||
}
|
||||
@@ -5,55 +5,68 @@ namespace Shlinkio\Shlink\CLI\Util;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use GeoIp2\Database\Reader;
|
||||
use InvalidArgumentException;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
use Throwable;
|
||||
|
||||
class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
||||
{
|
||||
private const LOCK_NAME = 'geolocation-db-update';
|
||||
|
||||
/** @var DbUpdaterInterface */
|
||||
private $dbUpdater;
|
||||
/** @var Reader */
|
||||
private $geoLiteDbReader;
|
||||
/** @var Locker */
|
||||
private $locker;
|
||||
|
||||
public function __construct(DbUpdaterInterface $dbUpdater, Reader $geoLiteDbReader)
|
||||
public function __construct(DbUpdaterInterface $dbUpdater, Reader $geoLiteDbReader, Locker $locker)
|
||||
{
|
||||
$this->dbUpdater = $dbUpdater;
|
||||
$this->geoLiteDbReader = $geoLiteDbReader;
|
||||
$this->locker = $locker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
public function checkDbUpdate(callable $mustBeUpdated = null, callable $handleProgress = null): void
|
||||
public function checkDbUpdate(?callable $mustBeUpdated = null, ?callable $handleProgress = null): void
|
||||
{
|
||||
$lock = $this->locker->createLock(self::LOCK_NAME);
|
||||
$lock->acquire(true); // Block until lock is released
|
||||
|
||||
try {
|
||||
$meta = $this->geoLiteDbReader->metadata();
|
||||
if ($this->buildIsTooOld($meta->__get('buildEpoch'))) {
|
||||
$this->downloadNewDb(true, $mustBeUpdated, $handleProgress);
|
||||
}
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// This is the exception thrown by the reader when the database file does not exist
|
||||
$this->downloadNewDb(false, $mustBeUpdated, $handleProgress);
|
||||
$this->downloadIfNeeded($mustBeUpdated, $handleProgress);
|
||||
} catch (Throwable $e) {
|
||||
throw $e;
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
}
|
||||
|
||||
private function buildIsTooOld(int $buildTimestamp): bool
|
||||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
private function downloadIfNeeded(?callable $mustBeUpdated, ?callable $handleProgress): void
|
||||
{
|
||||
$buildDate = Chronos::createFromTimestamp($buildTimestamp);
|
||||
$now = Chronos::now();
|
||||
return $now->gt($buildDate->addDays(35));
|
||||
if (! $this->dbUpdater->databaseFileExists()) {
|
||||
$this->downloadNewDb(false, $mustBeUpdated, $handleProgress);
|
||||
return;
|
||||
}
|
||||
|
||||
$meta = $this->geoLiteDbReader->metadata();
|
||||
if ($this->buildIsTooOld($meta->__get('buildEpoch'))) {
|
||||
$this->downloadNewDb(true, $mustBeUpdated, $handleProgress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
private function downloadNewDb(
|
||||
bool $olderDbExists,
|
||||
callable $mustBeUpdated = null,
|
||||
callable $handleProgress = null
|
||||
): void {
|
||||
private function downloadNewDb(bool $olderDbExists, ?callable $mustBeUpdated, ?callable $handleProgress): void
|
||||
{
|
||||
if ($mustBeUpdated !== null) {
|
||||
$mustBeUpdated($olderDbExists);
|
||||
}
|
||||
@@ -64,4 +77,11 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
||||
throw GeolocationDbUpdateFailedException::create($olderDbExists, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildIsTooOld(int $buildTimestamp): bool
|
||||
{
|
||||
$buildDate = Chronos::createFromTimestamp($buildTimestamp);
|
||||
$now = Chronos::now();
|
||||
return $now->gt($buildDate->addDays(35));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@ interface GeolocationDbUpdaterInterface
|
||||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
public function checkDbUpdate(callable $mustBeUpdated = null, callable $handleProgress = null): void;
|
||||
public function checkDbUpdate(?callable $mustBeUpdated = null, ?callable $handleProgress = null): void;
|
||||
}
|
||||
|
||||
155
module/CLI/test/Command/Db/CreateDatabaseCommandTest.php
Normal file
155
module/CLI/test/Command/Db/CreateDatabaseCommandTest.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Db;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Db\CreateDatabaseCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
use Symfony\Component\Lock\LockInterface;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
class CreateDatabaseCommandTest extends TestCase
|
||||
{
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $processHelper;
|
||||
/** @var ObjectProphecy */
|
||||
private $regularConn;
|
||||
/** @var ObjectProphecy */
|
||||
private $noDbNameConn;
|
||||
/** @var ObjectProphecy */
|
||||
private $schemaManager;
|
||||
/** @var ObjectProphecy */
|
||||
private $databasePlatform;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$locker = $this->prophesize(Locker::class);
|
||||
$lock = $this->prophesize(LockInterface::class);
|
||||
$lock->acquire(Argument::any())->willReturn(true);
|
||||
$lock->release()->will(function () {
|
||||
});
|
||||
$locker->createLock(Argument::cetera())->willReturn($lock->reveal());
|
||||
|
||||
$phpExecutableFinder = $this->prophesize(PhpExecutableFinder::class);
|
||||
$phpExecutableFinder->find(false)->willReturn('/usr/local/bin/php');
|
||||
|
||||
$this->processHelper = $this->prophesize(ProcessHelper::class);
|
||||
$this->schemaManager = $this->prophesize(AbstractSchemaManager::class);
|
||||
$this->databasePlatform = $this->prophesize(AbstractPlatform::class);
|
||||
|
||||
$this->regularConn = $this->prophesize(Connection::class);
|
||||
$this->regularConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
|
||||
$this->regularConn->getDatabasePlatform()->willReturn($this->databasePlatform->reveal());
|
||||
$this->noDbNameConn = $this->prophesize(Connection::class);
|
||||
$this->noDbNameConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
|
||||
|
||||
$command = new CreateDatabaseCommand(
|
||||
$locker->reveal(),
|
||||
$this->processHelper->reveal(),
|
||||
$phpExecutableFinder->reveal(),
|
||||
$this->regularConn->reveal(),
|
||||
$this->noDbNameConn->reveal()
|
||||
);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function successMessageIsPrintedIfDatabaseAlreadyExists(): void
|
||||
{
|
||||
$shlinkDatabase = 'shlink_database';
|
||||
$getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase);
|
||||
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', $shlinkDatabase, 'bar']);
|
||||
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function () {
|
||||
});
|
||||
$listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Database already exists. Run "db:migrate" command', $output);
|
||||
$getDatabase->shouldHaveBeenCalledOnce();
|
||||
$listDatabases->shouldHaveBeenCalledOnce();
|
||||
$createDatabase->shouldNotHaveBeenCalled();
|
||||
$listTables->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function databaseIsCreatedIfItDoesNotExist(): void
|
||||
{
|
||||
$shlinkDatabase = 'shlink_database';
|
||||
$getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase);
|
||||
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', 'bar']);
|
||||
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function () {
|
||||
});
|
||||
$listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$getDatabase->shouldHaveBeenCalledOnce();
|
||||
$listDatabases->shouldHaveBeenCalledOnce();
|
||||
$createDatabase->shouldHaveBeenCalledOnce();
|
||||
$listTables->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function tablesAreCreatedIfDatabaseIsEmpty(): void
|
||||
{
|
||||
$shlinkDatabase = 'shlink_database';
|
||||
$getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase);
|
||||
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', $shlinkDatabase, 'bar']);
|
||||
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function () {
|
||||
});
|
||||
$listTables = $this->schemaManager->listTableNames()->willReturn([]);
|
||||
$runCommand = $this->processHelper->run(Argument::type(OutputInterface::class), [
|
||||
'/usr/local/bin/php',
|
||||
CreateDatabaseCommand::DOCTRINE_HELPER_SCRIPT,
|
||||
CreateDatabaseCommand::DOCTRINE_HELPER_COMMAND,
|
||||
], Argument::cetera());
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Creating database tables...', $output);
|
||||
$this->assertStringContainsString('Database properly created!', $output);
|
||||
$getDatabase->shouldHaveBeenCalledOnce();
|
||||
$listDatabases->shouldHaveBeenCalledOnce();
|
||||
$createDatabase->shouldNotHaveBeenCalled();
|
||||
$listTables->shouldHaveBeenCalledOnce();
|
||||
$runCommand->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function databaseCheckIsSkippedForSqlite(): void
|
||||
{
|
||||
$this->databasePlatform->getName()->willReturn('sqlite');
|
||||
|
||||
$shlinkDatabase = 'shlink_database';
|
||||
$getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase);
|
||||
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', 'bar']);
|
||||
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function () {
|
||||
});
|
||||
$listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$getDatabase->shouldNotHaveBeenCalled();
|
||||
$listDatabases->shouldNotHaveBeenCalled();
|
||||
$createDatabase->shouldNotHaveBeenCalled();
|
||||
$listTables->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
82
module/CLI/test/Command/Db/MigrateDatabaseCommandTest.php
Normal file
82
module/CLI/test/Command/Db/MigrateDatabaseCommandTest.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Db;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Db\MigrateDatabaseCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
use Symfony\Component\Lock\LockInterface;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
class MigrateDatabaseCommandTest extends TestCase
|
||||
{
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $processHelper;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$locker = $this->prophesize(Locker::class);
|
||||
$lock = $this->prophesize(LockInterface::class);
|
||||
$lock->acquire(Argument::any())->willReturn(true);
|
||||
$lock->release()->will(function () {
|
||||
});
|
||||
$locker->createLock(Argument::cetera())->willReturn($lock->reveal());
|
||||
|
||||
$phpExecutableFinder = $this->prophesize(PhpExecutableFinder::class);
|
||||
$phpExecutableFinder->find(false)->willReturn('/usr/local/bin/php');
|
||||
|
||||
$this->processHelper = $this->prophesize(ProcessHelper::class);
|
||||
|
||||
$command = new MigrateDatabaseCommand(
|
||||
$locker->reveal(),
|
||||
$this->processHelper->reveal(),
|
||||
$phpExecutableFinder->reveal()
|
||||
);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideVerbosities
|
||||
*/
|
||||
public function migrationsCommandIsRunWithProperVerbosity(int $verbosity): void
|
||||
{
|
||||
$runCommand = $this->processHelper->run(Argument::type(OutputInterface::class), [
|
||||
'/usr/local/bin/php',
|
||||
MigrateDatabaseCommand::DOCTRINE_HELPER_SCRIPT,
|
||||
MigrateDatabaseCommand::DOCTRINE_HELPER_COMMAND,
|
||||
], null, null, $verbosity);
|
||||
|
||||
$this->commandTester->execute([], [
|
||||
'verbosity' => $verbosity,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
if ($verbosity >= OutputInterface::VERBOSITY_VERBOSE) {
|
||||
$this->assertStringContainsString('Migrating database...', $output);
|
||||
$this->assertStringContainsString('Database properly migrated!', $output);
|
||||
}
|
||||
$runCommand->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideVerbosities(): iterable
|
||||
{
|
||||
yield 'debug' => [OutputInterface::VERBOSITY_DEBUG];
|
||||
yield 'normal' => [OutputInterface::VERBOSITY_NORMAL];
|
||||
yield 'quiet' => [OutputInterface::VERBOSITY_QUIET];
|
||||
yield 'verbose' => [OutputInterface::VERBOSITY_VERBOSE];
|
||||
yield 'very verbose' => [OutputInterface::VERBOSITY_VERY_VERBOSE];
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,11 @@ use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
use function array_pop;
|
||||
use function sprintf;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
class DeleteShortUrlCommandTest extends TestCase
|
||||
{
|
||||
/** @var CommandTester */
|
||||
|
||||
@@ -49,10 +49,10 @@ class LocateVisitsCommandTest extends TestCase
|
||||
|
||||
$this->locker = $this->prophesize(Lock\Factory::class);
|
||||
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
||||
$this->lock->acquire()->willReturn(true);
|
||||
$this->lock->acquire(false)->willReturn(true);
|
||||
$this->lock->release()->will(function () {
|
||||
});
|
||||
$this->locker->createLock(Argument::type('string'))->willReturn($this->lock->reveal());
|
||||
$this->locker->createLock(Argument::type('string'), 90.0, false)->willReturn($this->lock->reveal());
|
||||
|
||||
$command = new LocateVisitsCommand(
|
||||
$this->visitService->reveal(),
|
||||
@@ -162,9 +162,9 @@ class LocateVisitsCommandTest extends TestCase
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function noActionIsPerformedIfLockIsAcquired()
|
||||
public function noActionIsPerformedIfLockIsAcquired(): void
|
||||
{
|
||||
$this->lock->acquire()->willReturn(false);
|
||||
$this->lock->acquire(false)->willReturn(false);
|
||||
|
||||
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function () {
|
||||
});
|
||||
@@ -174,7 +174,7 @@ class LocateVisitsCommandTest extends TestCase
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString(
|
||||
sprintf('There is already an instance of the "%s" command', LocateVisitsCommand::NAME),
|
||||
sprintf('Command "%s" is already in progress. Skipping.', LocateVisitsCommand::NAME),
|
||||
$output
|
||||
);
|
||||
$locateVisits->shouldNotHaveBeenCalled();
|
||||
|
||||
@@ -25,14 +25,7 @@ class ApplicationFactoryTest extends TestCase
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function serviceIsCreated()
|
||||
{
|
||||
$instance = ($this->factory)($this->createServiceManager(), '');
|
||||
$this->assertInstanceOf(Application::class, $instance);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function allCommandsWhichAreServicesAreAdded()
|
||||
public function allCommandsWhichAreServicesAreAdded(): void
|
||||
{
|
||||
$sm = $this->createServiceManager([
|
||||
'commands' => [
|
||||
@@ -45,8 +38,7 @@ class ApplicationFactoryTest extends TestCase
|
||||
$sm->setService('bar', $this->createCommandMock('bar')->reveal());
|
||||
|
||||
/** @var Application $instance */
|
||||
$instance = ($this->factory)($sm, '');
|
||||
$this->assertInstanceOf(Application::class, $instance);
|
||||
$instance = ($this->factory)($sm);
|
||||
|
||||
$this->assertTrue($instance->has('foo'));
|
||||
$this->assertTrue($instance->has('bar'));
|
||||
|
||||
29
module/CLI/test/Factory/ProcessHelperFactoryTest.php
Normal file
29
module/CLI/test/Factory/ProcessHelperFactoryTest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Factory;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
|
||||
|
||||
class ProcessHelperFactoryTest extends TestCase
|
||||
{
|
||||
/** @var ProcessHelperFactory */
|
||||
private $factory;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->factory = new ProcessHelperFactory();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function createsTheServiceWithTheProperSetOfHelpers(): void
|
||||
{
|
||||
$processHelper = ($this->factory)();
|
||||
$helperSet = $processHelper->getHelperSet();
|
||||
|
||||
$this->assertCount(2, $helperSet);
|
||||
$this->assertTrue($helperSet->has('formatter'));
|
||||
$this->assertTrue($helperSet->has('debug_formatter'));
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,15 @@ namespace ShlinkioTest\Shlink\CLI\Util;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use GeoIp2\Database\Reader;
|
||||
use InvalidArgumentException;
|
||||
use MaxMind\Db\Reader\Metadata;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||
use Symfony\Component\Lock;
|
||||
use Throwable;
|
||||
|
||||
use function Functional\map;
|
||||
@@ -26,15 +27,27 @@ class GeolocationDbUpdaterTest extends TestCase
|
||||
private $dbUpdater;
|
||||
/** @var ObjectProphecy */
|
||||
private $geoLiteDbReader;
|
||||
/** @var ObjectProphecy */
|
||||
private $locker;
|
||||
/** @var ObjectProphecy */
|
||||
private $lock;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
|
||||
$this->geoLiteDbReader = $this->prophesize(Reader::class);
|
||||
|
||||
$this->locker = $this->prophesize(Lock\Factory::class);
|
||||
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
||||
$this->lock->acquire(true)->willReturn(true);
|
||||
$this->lock->release()->will(function () {
|
||||
});
|
||||
$this->locker->createLock(Argument::type('string'))->willReturn($this->lock->reveal());
|
||||
|
||||
$this->geolocationDbUpdater = new GeolocationDbUpdater(
|
||||
$this->dbUpdater->reveal(),
|
||||
$this->geoLiteDbReader->reveal()
|
||||
$this->geoLiteDbReader->reveal(),
|
||||
$this->locker->reveal()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,8 +57,10 @@ class GeolocationDbUpdaterTest extends TestCase
|
||||
$mustBeUpdated = function () {
|
||||
$this->assertTrue(true);
|
||||
};
|
||||
$getMeta = $this->geoLiteDbReader->metadata()->willThrow(InvalidArgumentException::class);
|
||||
$prev = new RuntimeException('');
|
||||
|
||||
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(false);
|
||||
$getMeta = $this->geoLiteDbReader->metadata();
|
||||
$download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev);
|
||||
|
||||
try {
|
||||
@@ -58,7 +73,8 @@ class GeolocationDbUpdaterTest extends TestCase
|
||||
$this->assertFalse($e->olderDbExists());
|
||||
}
|
||||
|
||||
$getMeta->shouldHaveBeenCalledOnce();
|
||||
$fileExists->shouldHaveBeenCalledOnce();
|
||||
$getMeta->shouldNotHaveBeenCalled();
|
||||
$download->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
@@ -68,6 +84,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
||||
*/
|
||||
public function exceptionIsThrownWhenOlderDbIsTooOldAndDownloadFails(int $days): void
|
||||
{
|
||||
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(true);
|
||||
$getMeta = $this->geoLiteDbReader->metadata()->willReturn(new Metadata([
|
||||
'binary_format_major_version' => '',
|
||||
'binary_format_minor_version' => '',
|
||||
@@ -92,6 +109,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
||||
$this->assertTrue($e->olderDbExists());
|
||||
}
|
||||
|
||||
$fileExists->shouldHaveBeenCalledOnce();
|
||||
$getMeta->shouldHaveBeenCalledOnce();
|
||||
$download->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
@@ -110,6 +128,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
||||
*/
|
||||
public function databaseIsNotUpdatedIfItIsYoungerThanOneWeek(int $days): void
|
||||
{
|
||||
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(true);
|
||||
$getMeta = $this->geoLiteDbReader->metadata()->willReturn(new Metadata([
|
||||
'binary_format_major_version' => '',
|
||||
'binary_format_minor_version' => '',
|
||||
@@ -126,6 +145,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
||||
|
||||
$this->geolocationDbUpdater->checkDbUpdate();
|
||||
|
||||
$fileExists->shouldHaveBeenCalledOnce();
|
||||
$getMeta->shouldHaveBeenCalledOnce();
|
||||
$download->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
17
module/Common/config/cache.config.php
Normal file
17
module/Common/config/cache.config.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common;
|
||||
|
||||
use Doctrine\Common\Cache as DoctrineCache;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
DoctrineCache\Cache::class => Cache\CacheFactory::class,
|
||||
Cache\RedisFactory::SERVICE_NAME => Cache\RedisFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -3,8 +3,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common;
|
||||
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use GeoIp2\Database\Reader;
|
||||
use GuzzleHttp\Client as GuzzleClient;
|
||||
use Monolog\Logger;
|
||||
@@ -20,9 +18,7 @@ return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
EntityManager::class => Factory\EntityManagerFactory::class,
|
||||
GuzzleClient::class => InvokableFactory::class,
|
||||
Cache::class => Factory\CacheFactory::class,
|
||||
Filesystem::class => InvokableFactory::class,
|
||||
Reader::class => ConfigAbstractFactory::class,
|
||||
|
||||
@@ -45,7 +41,6 @@ return [
|
||||
Service\PreviewGenerator::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'em' => EntityManager::class,
|
||||
'httpClient' => GuzzleClient::class,
|
||||
'translator' => Translator::class,
|
||||
|
||||
|
||||
35
module/Common/config/doctrine.config.php
Normal file
35
module/Common/config/doctrine.config.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
|
||||
return [
|
||||
|
||||
'entity_manager' => [
|
||||
'orm' => [
|
||||
'types' => [
|
||||
Type\ChronosDateTimeType::CHRONOS_DATETIME => Type\ChronosDateTimeType::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
EntityManager::class => Doctrine\EntityManagerFactory::class,
|
||||
Connection::class => Doctrine\ConnectionFactory::class,
|
||||
Doctrine\NoDbNameConnectionFactory::SERVICE_NAME => Doctrine\NoDbNameConnectionFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'em' => EntityManager::class,
|
||||
],
|
||||
'delegators' => [
|
||||
EntityManager::class => [
|
||||
Doctrine\ReopeningEntityManagerDelegator::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -3,8 +3,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common;
|
||||
|
||||
use const JSON_ERROR_NONE;
|
||||
|
||||
use function getenv;
|
||||
use function json_decode as spl_json_decode;
|
||||
use function json_last_error;
|
||||
@@ -13,6 +11,8 @@ use function sprintf;
|
||||
use function strtolower;
|
||||
use function trim;
|
||||
|
||||
use const JSON_ERROR_NONE;
|
||||
|
||||
/**
|
||||
* Gets the value of an environment variable. Supports boolean, empty and null.
|
||||
* This is basically Laravel's env helper
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Factory;
|
||||
namespace Shlinkio\Shlink\Common\Cache;
|
||||
|
||||
use Doctrine\Common\Cache;
|
||||
use Interop\Container\ContainerInterface;
|
||||
@@ -12,8 +12,10 @@ use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
class CacheFactory implements FactoryInterface
|
||||
{
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null): Cache\Cache
|
||||
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): Cache\Cache
|
||||
{
|
||||
// TODO Make use of the redis cache via RedisFactory when possible
|
||||
|
||||
$appOptions = $container->get(AppOptions::class);
|
||||
$adapter = env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
|
||||
$adapter->setNamespace((string) $appOptions);
|
||||
27
module/Common/src/Cache/RedisFactory.php
Normal file
27
module/Common/src/Cache/RedisFactory.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Cache;
|
||||
|
||||
use Predis\Client as PredisClient;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
use function count;
|
||||
use function explode;
|
||||
use function is_string;
|
||||
|
||||
class RedisFactory
|
||||
{
|
||||
public const SERVICE_NAME = 'Shlinkio\Shlink\Common\Cache\Redis';
|
||||
|
||||
public function __invoke(ContainerInterface $container): PredisClient
|
||||
{
|
||||
$redisConfig = $container->get('config')['redis'] ?? [];
|
||||
|
||||
$servers = $redisConfig['servers'] ?? [];
|
||||
$servers = is_string($servers) ? explode(',', $servers) : $servers;
|
||||
$options = count($servers) <= 1 ? null : ['cluster' => 'redis'];
|
||||
|
||||
return new PredisClient($servers, $options);
|
||||
}
|
||||
}
|
||||
17
module/Common/src/Doctrine/ConnectionFactory.php
Normal file
17
module/Common/src/Doctrine/ConnectionFactory.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class ConnectionFactory
|
||||
{
|
||||
public function __invoke(ContainerInterface $container): Connection
|
||||
{
|
||||
$em = $container->get(EntityManager::class);
|
||||
return $em->getConnection();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Factory;
|
||||
namespace Shlinkio\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\Common\Cache\ArrayCache;
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
@@ -11,36 +11,42 @@ use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Tools\Setup;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class EntityManagerFactory implements FactoryInterface
|
||||
class EntityManagerFactory
|
||||
{
|
||||
/**
|
||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
|
||||
* @throws ORMException
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
public function __invoke(ContainerInterface $container): EntityManager
|
||||
{
|
||||
$globalConfig = $container->get('config');
|
||||
$isDevMode = isset($globalConfig['debug']) ? ((bool) $globalConfig['debug']) : false;
|
||||
$isDevMode = (bool) ($globalConfig['debug'] ?? false);
|
||||
$cache = $container->has(Cache::class) ? $container->get(Cache::class) : new ArrayCache();
|
||||
$emConfig = $globalConfig['entity_manager'] ?? [];
|
||||
$connectionConfig = $emConfig['connection'] ?? [];
|
||||
$ormConfig = $emConfig['orm'] ?? [];
|
||||
|
||||
if (! Type::hasType(ChronosDateTimeType::CHRONOS_DATETIME)) {
|
||||
Type::addType(ChronosDateTimeType::CHRONOS_DATETIME, ChronosDateTimeType::class);
|
||||
}
|
||||
$this->registerTypes($ormConfig);
|
||||
|
||||
$config = Setup::createConfiguration($isDevMode, $ormConfig['proxies_dir'] ?? null, $cache);
|
||||
$config->setMetadataDriverImpl(new PHPDriver($ormConfig['entities_mappings'] ?? []));
|
||||
|
||||
return EntityManager::create($connectionConfig, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DBALException
|
||||
*/
|
||||
private function registerTypes(array $ormConfig): void
|
||||
{
|
||||
$types = $ormConfig['types'] ?? [];
|
||||
|
||||
foreach ($types as $name => $className) {
|
||||
if (! Type::hasType($name)) {
|
||||
Type::addType($name, $className);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
module/Common/src/Doctrine/NoDbNameConnectionFactory.php
Normal file
21
module/Common/src/Doctrine/NoDbNameConnectionFactory.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class NoDbNameConnectionFactory
|
||||
{
|
||||
public const SERVICE_NAME = 'Shlinkio\Shlink\Common\Doctrine\NoDbNameConnection';
|
||||
|
||||
public function __invoke(ContainerInterface $container): Connection
|
||||
{
|
||||
$conn = $container->get(Connection::class);
|
||||
$params = $conn->getParams();
|
||||
unset($params['dbname']);
|
||||
|
||||
return new Connection($params, $conn->getDriver(), $conn->getConfiguration(), $conn->getEventManager());
|
||||
}
|
||||
}
|
||||
57
module/Common/src/Doctrine/ReopeningEntityManager.php
Normal file
57
module/Common/src/Doctrine/ReopeningEntityManager.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\ORM\Decorator\EntityManagerDecorator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class ReopeningEntityManager extends EntityManagerDecorator
|
||||
{
|
||||
/** @var callable */
|
||||
private $emFactory;
|
||||
|
||||
public function __construct(EntityManagerInterface $wrapped, callable $emFactory)
|
||||
{
|
||||
parent::__construct($wrapped);
|
||||
$this->emFactory = $emFactory;
|
||||
}
|
||||
|
||||
protected function getWrappedEntityManager(): EntityManagerInterface
|
||||
{
|
||||
if (! $this->wrapped->isOpen()) {
|
||||
$this->wrapped = ($this->emFactory)(
|
||||
$this->wrapped->getConnection(),
|
||||
$this->wrapped->getConfiguration(),
|
||||
$this->wrapped->getEventManager()
|
||||
);
|
||||
}
|
||||
|
||||
return $this->wrapped;
|
||||
}
|
||||
|
||||
public function flush($entity = null): void
|
||||
{
|
||||
$this->getWrappedEntityManager()->flush($entity);
|
||||
}
|
||||
|
||||
public function persist($object): void
|
||||
{
|
||||
$this->getWrappedEntityManager()->persist($object);
|
||||
}
|
||||
|
||||
public function remove($object): void
|
||||
{
|
||||
$this->getWrappedEntityManager()->remove($object);
|
||||
}
|
||||
|
||||
public function refresh($object): void
|
||||
{
|
||||
$this->getWrappedEntityManager()->refresh($object);
|
||||
}
|
||||
|
||||
public function merge($object)
|
||||
{
|
||||
return $this->getWrappedEntityManager()->merge($object);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class ReopeningEntityManagerDelegator
|
||||
{
|
||||
public function __invoke(ContainerInterface $container, string $name, callable $callback): ReopeningEntityManager
|
||||
{
|
||||
return new ReopeningEntityManager($callback(), [EntityManager::class, 'create']);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@ abstract class AbstractEntity
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setId(string $id): self
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
@@ -9,7 +9,7 @@ use function sprintf;
|
||||
|
||||
class WrongIpException extends RuntimeException
|
||||
{
|
||||
public static function fromIpAddress($ipAddress, Throwable $prev = null): self
|
||||
public static function fromIpAddress($ipAddress, ?Throwable $prev = null): self
|
||||
{
|
||||
return new self(sprintf('Provided IP "%s" is invalid', $ipAddress), 0, $prev);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class DottedAccessConfigAbstractFactory implements AbstractFactoryInterface
|
||||
* creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null)
|
||||
{
|
||||
$parts = explode('.', $requestedName);
|
||||
$serviceName = array_shift($parts);
|
||||
|
||||
@@ -25,7 +25,7 @@ class EmptyResponseImplicitOptionsMiddlewareFactory implements FactoryInterface
|
||||
* creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null)
|
||||
{
|
||||
return new ImplicitOptionsMiddleware(function () {
|
||||
return new EmptyResponse();
|
||||
|
||||
@@ -24,7 +24,7 @@ class TranslatorFactory implements FactoryInterface
|
||||
* creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null)
|
||||
{
|
||||
$config = $container->get('config');
|
||||
return Translator::factory($config['translator'] ?? []);
|
||||
|
||||
@@ -24,7 +24,7 @@ class ImageBuilderFactory implements FactoryInterface
|
||||
* creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null)
|
||||
{
|
||||
return new ImageBuilder($container, ['factories' => [
|
||||
Image::class => ImageFactory::class,
|
||||
|
||||
@@ -24,7 +24,7 @@ class ImageFactory implements FactoryInterface
|
||||
* creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null)
|
||||
{
|
||||
$config = $container->get('config')['wkhtmltopdf'];
|
||||
$image = new Image($config['images'] ?? null);
|
||||
|
||||
@@ -37,7 +37,7 @@ class DbUpdater implements DbUpdaterInterface
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function downloadFreshCopy(callable $handleProgress = null): void
|
||||
public function downloadFreshCopy(?callable $handleProgress = null): void
|
||||
{
|
||||
$tempDir = $this->options->getTempDir();
|
||||
$compressedFile = sprintf('%s/%s', $tempDir, self::DB_COMPRESSED_FILE);
|
||||
@@ -48,7 +48,7 @@ class DbUpdater implements DbUpdaterInterface
|
||||
$this->deleteTempFiles([$compressedFile, $tempFullPath]);
|
||||
}
|
||||
|
||||
private function downloadDbFile(string $dest, callable $handleProgress = null): void
|
||||
private function downloadDbFile(string $dest, ?callable $handleProgress = null): void
|
||||
{
|
||||
try {
|
||||
$this->httpClient->request(RequestMethod::METHOD_GET, $this->options->getDownloadFrom(), [
|
||||
@@ -98,4 +98,9 @@ class DbUpdater implements DbUpdaterInterface
|
||||
// Ignore any error produced when trying to delete temp files
|
||||
}
|
||||
}
|
||||
|
||||
public function databaseFileExists(): bool
|
||||
{
|
||||
return $this->filesystem->exists($this->options->getDbLocation());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||
|
||||
interface DbUpdaterInterface
|
||||
{
|
||||
public function databaseFileExists(): bool;
|
||||
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function downloadFreshCopy(callable $handleProgress = null): void;
|
||||
public function downloadFreshCopy(?callable $handleProgress = null): void;
|
||||
}
|
||||
|
||||
18
module/Common/src/Lock/RetryLockStoreDelegatorFactory.php
Normal file
18
module/Common/src/Lock/RetryLockStoreDelegatorFactory.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Lock;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Symfony\Component\Lock\Store\RetryTillSaveStore;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
class RetryLockStoreDelegatorFactory
|
||||
{
|
||||
public function __invoke(ContainerInterface $container, $name, callable $callback): RetryTillSaveStore
|
||||
{
|
||||
/** @var StoreInterface $originalStore */
|
||||
$originalStore = $callback();
|
||||
return new RetryTillSaveStore($originalStore);
|
||||
}
|
||||
}
|
||||
20
module/Common/src/Logger/LoggerAwareDelegatorFactory.php
Normal file
20
module/Common/src/Logger/LoggerAwareDelegatorFactory.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Logger;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Log;
|
||||
|
||||
class LoggerAwareDelegatorFactory
|
||||
{
|
||||
public function __invoke(ContainerInterface $container, $name, callable $callback)
|
||||
{
|
||||
$instance = $callback();
|
||||
if ($instance instanceof Log\LoggerAwareInterface) {
|
||||
$instance->setLogger($container->get(Log\LoggerInterface::class));
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Factory;
|
||||
namespace Shlinkio\Shlink\Common\Logger;
|
||||
|
||||
use Cascade\Cascade;
|
||||
use Interop\Container\ContainerInterface;
|
||||
@@ -27,7 +27,7 @@ class LoggerFactory implements FactoryInterface
|
||||
* creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null)
|
||||
{
|
||||
$config = $container->has('config') ? $container->get('config') : [];
|
||||
Cascade::fileConfig($config['logger'] ?? ['loggers' => []]);
|
||||
@@ -3,11 +3,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Logger\Processor;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
use function str_replace;
|
||||
use function strpos;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
final class ExceptionWithNewLineProcessor
|
||||
{
|
||||
private const EXCEPTION_PLACEHOLDER = '{e}';
|
||||
|
||||
@@ -19,16 +19,13 @@ class CloseDbConnectionMiddleware implements MiddlewareInterface
|
||||
$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;
|
||||
try {
|
||||
return $handler->handle($request);
|
||||
} finally {
|
||||
$this->em->getConnection()->close();
|
||||
$this->em->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,10 @@ class IpAddressMiddlewareFactory implements FactoryInterface
|
||||
* @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): IpAddress
|
||||
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): IpAddress
|
||||
{
|
||||
return new IpAddress(true, [], Visitor::REMOTE_ADDRESS_ATTR);
|
||||
$config = $container->get('config');
|
||||
$headersToInspect = $config['ip_address_resolution']['headers_to_inspect'] ?? [];
|
||||
return new IpAddress(true, [], Visitor::REMOTE_ADDRESS_ATTR, $headersToInspect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ interface PaginableRepositoryInterface
|
||||
* @return array
|
||||
*/
|
||||
public function findList(
|
||||
int $limit = null,
|
||||
int $offset = null,
|
||||
string $searchTerm = null,
|
||||
?int $limit = null,
|
||||
?int $offset = null,
|
||||
?string $searchTerm = null,
|
||||
array $tags = [],
|
||||
$orderBy = null
|
||||
): array;
|
||||
@@ -30,5 +30,5 @@ interface PaginableRepositoryInterface
|
||||
* @param array $tags
|
||||
* @return int
|
||||
*/
|
||||
public function countList(string $searchTerm = null, array $tags = []): int;
|
||||
public function countList(?string $searchTerm = null, array $tags = []): int;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Factory;
|
||||
namespace ShlinkioTest\Shlink\Common\Cache;
|
||||
|
||||
use Doctrine\Common\Cache\ApcuCache;
|
||||
use Doctrine\Common\Cache\ArrayCache;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Factory\CacheFactory;
|
||||
use Shlinkio\Shlink\Common\Cache\CacheFactory;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
61
module/Common/test/Cache/RedisFactoryTest.php
Normal file
61
module/Common/test/Cache/RedisFactoryTest.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Cache;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Predis\Connection\Aggregate\PredisCluster;
|
||||
use Predis\Connection\Aggregate\RedisCluster;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\Common\Cache\RedisFactory;
|
||||
|
||||
class RedisFactoryTest extends TestCase
|
||||
{
|
||||
/** @var RedisFactory */
|
||||
private $factory;
|
||||
/** @var ObjectProphecy */
|
||||
private $container;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->container = $this->prophesize(ContainerInterface::class);
|
||||
$this->factory = new RedisFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideRedisConfig
|
||||
*/
|
||||
public function createsRedisClientBasedOnConfig(?array $config, string $expectedCluster): void
|
||||
{
|
||||
$getConfig = $this->container->get('config')->willReturn([
|
||||
'redis' => $config,
|
||||
]);
|
||||
|
||||
$client = ($this->factory)($this->container->reveal());
|
||||
|
||||
$getConfig->shouldHaveBeenCalledOnce();
|
||||
$this->assertInstanceOf($expectedCluster, $client->getOptions()->cluster);
|
||||
}
|
||||
|
||||
public function provideRedisConfig(): iterable
|
||||
{
|
||||
yield 'no config' => [null, PredisCluster::class];
|
||||
yield 'single server as string' => [[
|
||||
'servers' => 'tcp://127.0.0.1:6379',
|
||||
], PredisCluster::class];
|
||||
yield 'single server as array' => [[
|
||||
'servers' => ['tcp://127.0.0.1:6379'],
|
||||
], PredisCluster::class];
|
||||
yield 'cluster of servers' => [[
|
||||
'servers' => ['tcp://1.1.1.1:6379', 'tcp://2.2.2.2:6379'],
|
||||
], RedisCluster::class];
|
||||
yield 'empty cluster of servers' => [[
|
||||
'servers' => [],
|
||||
], PredisCluster::class];
|
||||
yield 'cluster of servers as string' => [[
|
||||
'servers' => 'tcp://1.1.1.1:6379,tcp://2.2.2.2:6379',
|
||||
], RedisCluster::class];
|
||||
}
|
||||
}
|
||||
44
module/Common/test/Doctrine/ConnectionFactoryTest.php
Normal file
44
module/Common/test/Doctrine/ConnectionFactoryTest.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\Common\Doctrine\ConnectionFactory;
|
||||
|
||||
class ConnectionFactoryTest extends TestCase
|
||||
{
|
||||
/** @var ConnectionFactory */
|
||||
private $factory;
|
||||
/** @var ObjectProphecy */
|
||||
private $container;
|
||||
/** @var ObjectProphecy */
|
||||
private $em;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->container = $this->prophesize(ContainerInterface::class);
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->container->get(EntityManager::class)->willReturn($this->em->reveal());
|
||||
|
||||
$this->factory = new ConnectionFactory();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function properServiceFallbackOccursWhenInvoked(): void
|
||||
{
|
||||
$connection = $this->prophesize(Connection::class)->reveal();
|
||||
$getConnection = $this->em->getConnection()->willReturn($connection);
|
||||
|
||||
$result = ($this->factory)($this->container->reveal());
|
||||
|
||||
$this->assertSame($connection, $result);
|
||||
$getConnection->shouldHaveBeenCalledOnce();
|
||||
$this->container->get(EntityManager::class)->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Factory;
|
||||
namespace ShlinkioTest\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Factory\EntityManagerFactory;
|
||||
use Shlinkio\Shlink\Common\Doctrine\EntityManagerFactory;
|
||||
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class EntityManagerFactoryTest extends TestCase
|
||||
@@ -19,12 +20,17 @@ class EntityManagerFactoryTest extends TestCase
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function serviceIsCreated()
|
||||
public function serviceIsCreated(): void
|
||||
{
|
||||
$sm = new ServiceManager(['services' => [
|
||||
'config' => [
|
||||
'debug' => true,
|
||||
'entity_manager' => [
|
||||
'orm' => [
|
||||
'types' => [
|
||||
ChronosDateTimeType::CHRONOS_DATETIME => ChronosDateTimeType::class,
|
||||
],
|
||||
],
|
||||
'connection' => [
|
||||
'driver' => 'pdo_sqlite',
|
||||
],
|
||||
@@ -32,7 +38,7 @@ class EntityManagerFactoryTest extends TestCase
|
||||
],
|
||||
]]);
|
||||
|
||||
$em = $this->factory->__invoke($sm, EntityManager::class);
|
||||
$em = ($this->factory)($sm, EntityManager::class);
|
||||
$this->assertInstanceOf(EntityManager::class, $em);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
||||
|
||||
class NoDbNameConnectionFactoryTest extends TestCase
|
||||
{
|
||||
/** @var NoDbNameConnectionFactory */
|
||||
private $factory;
|
||||
/** @var ObjectProphecy */
|
||||
private $container;
|
||||
/** @var ObjectProphecy */
|
||||
private $originalConn;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->container = $this->prophesize(ContainerInterface::class);
|
||||
$this->originalConn = $this->prophesize(Connection::class);
|
||||
$this->container->get(Connection::class)->willReturn($this->originalConn->reveal());
|
||||
|
||||
$this->factory = new NoDbNameConnectionFactory();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function createsNewConnectionRemovingDbNameFromOriginalConnectionParams(): void
|
||||
{
|
||||
$params = [
|
||||
'username' => 'foo',
|
||||
'password' => 'bar',
|
||||
'dbname' => 'something',
|
||||
];
|
||||
$getOriginalParams = $this->originalConn->getParams()->willReturn($params);
|
||||
$getOriginalDriver = $this->originalConn->getDriver()->willReturn($this->prophesize(Driver::class)->reveal());
|
||||
$getOriginalConfig = $this->originalConn->getConfiguration()->willReturn(null);
|
||||
$getOriginalEvents = $this->originalConn->getEventManager()->willReturn(null);
|
||||
|
||||
$conn = ($this->factory)($this->container->reveal());
|
||||
|
||||
$this->assertEquals([
|
||||
'username' => 'foo',
|
||||
'password' => 'bar',
|
||||
], $conn->getParams());
|
||||
$getOriginalParams->shouldHaveBeenCalledOnce();
|
||||
$getOriginalDriver->shouldHaveBeenCalledOnce();
|
||||
$getOriginalConfig->shouldHaveBeenCalledOnce();
|
||||
$getOriginalEvents->shouldHaveBeenCalledOnce();
|
||||
$this->container->get(Connection::class)->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionObject;
|
||||
use Shlinkio\Shlink\Common\Doctrine\ReopeningEntityManagerDelegator;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class ReopeningEntityManagerDelegatorTest extends TestCase
|
||||
{
|
||||
/** @test */
|
||||
public function decoratesEntityManagerFromCallback(): void
|
||||
{
|
||||
$em = $this->prophesize(EntityManagerInterface::class)->reveal();
|
||||
$result = (new ReopeningEntityManagerDelegator())(new ServiceManager(), '', function () use ($em) {
|
||||
return $em;
|
||||
});
|
||||
|
||||
$ref = new ReflectionObject($result);
|
||||
$prop = $ref->getProperty('wrapped');
|
||||
$prop->setAccessible(true);
|
||||
|
||||
$this->assertSame($em, $prop->getValue($result));
|
||||
}
|
||||
}
|
||||
80
module/Common/test/Doctrine/ReopeningEntityManagerTest.php
Normal file
80
module/Common/test/Doctrine/ReopeningEntityManagerTest.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\Common\EventManager;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\Configuration;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Common\Doctrine\ReopeningEntityManager;
|
||||
use stdClass;
|
||||
|
||||
class ReopeningEntityManagerTest extends TestCase
|
||||
{
|
||||
/** @var ReopeningEntityManager */
|
||||
private $decoratorEm;
|
||||
/** @var ObjectProphecy */
|
||||
private $wrapped;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->wrapped = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->wrapped->getConnection()->willReturn($this->prophesize(Connection::class));
|
||||
$this->wrapped->getConfiguration()->willReturn($this->prophesize(Configuration::class));
|
||||
$this->wrapped->getEventManager()->willReturn($this->prophesize(EventManager::class));
|
||||
|
||||
$wrappedMock = $this->wrapped->reveal();
|
||||
$this->decoratorEm = new ReopeningEntityManager($wrappedMock, function () use ($wrappedMock) {
|
||||
return $wrappedMock;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideMethodNames
|
||||
*/
|
||||
public function wrappedInstanceIsTransparentlyCalledWhenItIsNotClosed(string $methodName): void
|
||||
{
|
||||
$method = $this->wrapped->__call($methodName, [Argument::cetera()])->willReturnArgument();
|
||||
$isOpen = $this->wrapped->isOpen()->willReturn(true);
|
||||
|
||||
$this->decoratorEm->{$methodName}(new stdClass());
|
||||
|
||||
$method->shouldHaveBeenCalledOnce();
|
||||
$isOpen->shouldHaveBeenCalledOnce();
|
||||
$this->wrapped->getConnection()->shouldNotHaveBeenCalled();
|
||||
$this->wrapped->getConfiguration()->shouldNotHaveBeenCalled();
|
||||
$this->wrapped->getEventManager()->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideMethodNames
|
||||
*/
|
||||
public function wrappedInstanceIsRecreatedWhenItIsClosed(string $methodName): void
|
||||
{
|
||||
$method = $this->wrapped->__call($methodName, [Argument::cetera()])->willReturnArgument();
|
||||
$isOpen = $this->wrapped->isOpen()->willReturn(false);
|
||||
|
||||
$this->decoratorEm->{$methodName}(new stdClass());
|
||||
|
||||
$method->shouldHaveBeenCalledOnce();
|
||||
$isOpen->shouldHaveBeenCalledOnce();
|
||||
$this->wrapped->getConnection()->shouldHaveBeenCalledOnce();
|
||||
$this->wrapped->getConfiguration()->shouldHaveBeenCalledOnce();
|
||||
$this->wrapped->getEventManager()->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideMethodNames(): iterable
|
||||
{
|
||||
yield 'flush' => ['flush'];
|
||||
yield 'persist' => ['persist'];
|
||||
yield 'remove' => ['remove'];
|
||||
yield 'refresh' => ['refresh'];
|
||||
yield 'merge' => ['merge'];
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class DbUpdaterTest extends TestCase
|
||||
$this->filesystem = $this->prophesize(Filesystem::class);
|
||||
$this->options = new GeoLite2Options([
|
||||
'temp_dir' => __DIR__ . '/../../../test-resources',
|
||||
'db_location' => '',
|
||||
'db_location' => 'db_location',
|
||||
'download_from' => '',
|
||||
]);
|
||||
|
||||
@@ -110,4 +110,23 @@ class DbUpdaterTest extends TestCase
|
||||
$copy->shouldHaveBeenCalledOnce();
|
||||
$remove->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideExists
|
||||
*/
|
||||
public function databaseFileExistsChecksIfTheFilesExistsInTheFilesystem(bool $expected): void
|
||||
{
|
||||
$exists = $this->filesystem->exists('db_location')->willReturn($expected);
|
||||
|
||||
$result = $this->dbUpdater->databaseFileExists();
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
$exists->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideExists(): iterable
|
||||
{
|
||||
return [[true], [false]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Lock;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use ReflectionObject;
|
||||
use Shlinkio\Shlink\Common\Lock\RetryLockStoreDelegatorFactory;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class RetryLockStoreDelegatorFactoryTest extends TestCase
|
||||
{
|
||||
/** @var RetryLockStoreDelegatorFactory */
|
||||
private $delegator;
|
||||
/** @var ObjectProphecy */
|
||||
private $originalStore;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->originalStore = $this->prophesize(StoreInterface::class)->reveal();
|
||||
$this->delegator = new RetryLockStoreDelegatorFactory();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function originalStoreIsWrappedInRetryStore(): void
|
||||
{
|
||||
$callback = function () {
|
||||
return $this->originalStore;
|
||||
};
|
||||
|
||||
$result = ($this->delegator)(new ServiceManager(), '', $callback);
|
||||
|
||||
$ref = new ReflectionObject($result);
|
||||
$prop = $ref->getProperty('decorated');
|
||||
$prop->setAccessible(true);
|
||||
|
||||
$this->assertSame($this->originalStore, $prop->getValue($result));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Logger;
|
||||
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Log;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory;
|
||||
use stdClass;
|
||||
|
||||
class LoggerAwareDelegatorFactoryTest extends TestCase
|
||||
{
|
||||
/** @var LoggerAwareDelegatorFactory */
|
||||
private $delegator;
|
||||
/** @var ObjectProphecy */
|
||||
private $container;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->container = $this->prophesize(ContainerInterface::class);
|
||||
$this->delegator = new LoggerAwareDelegatorFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideInstances
|
||||
*/
|
||||
public function injectsLoggerOnInstanceWhenImplementingLoggerAware($instance, int $expectedCalls): void
|
||||
{
|
||||
$callback = function () use ($instance) {
|
||||
return $instance;
|
||||
};
|
||||
$getLogger = $this->container->get(Log\LoggerInterface::class)->willReturn(new Log\NullLogger());
|
||||
|
||||
$result = ($this->delegator)($this->container->reveal(), '', $callback);
|
||||
|
||||
$this->assertSame($instance, $result);
|
||||
$getLogger->shouldHaveBeenCalledTimes($expectedCalls);
|
||||
}
|
||||
|
||||
public function provideInstances(): iterable
|
||||
{
|
||||
yield 'no logger aware' => [new stdClass(), 0];
|
||||
yield 'logger aware' => [new class implements Log\LoggerAwareInterface {
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
Assert::assertInstanceOf(Log\NullLogger::class, $logger);
|
||||
}
|
||||
}, 1];
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Factory;
|
||||
namespace ShlinkioTest\Shlink\Common\Logger;
|
||||
|
||||
use Monolog\Logger;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Common\Factory\LoggerFactory;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerFactory;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class LoggerFactoryTest extends TestCase
|
||||
@@ -7,11 +7,11 @@ use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Logger\Processor\ExceptionWithNewLineProcessor;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
class ExceptionWithNewLineProcessorTest extends TestCase
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
@@ -8,6 +8,7 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use RuntimeException;
|
||||
use Shlinkio\Shlink\Common\Middleware\CloseDbConnectionMiddleware;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\ServerRequest;
|
||||
@@ -20,35 +21,52 @@ class CloseDbConnectionMiddlewareTest extends TestCase
|
||||
private $handler;
|
||||
/** @var ObjectProphecy */
|
||||
private $em;
|
||||
/** @var ObjectProphecy */
|
||||
private $conn;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->handler = $this->prophesize(RequestHandlerInterface::class);
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->conn = $this->prophesize(Connection::class);
|
||||
$this->conn->close()->will(function () {
|
||||
});
|
||||
$this->em->getConnection()->willReturn($this->conn->reveal());
|
||||
$this->em->clear()->will(function () {
|
||||
});
|
||||
|
||||
$this->middleware = new CloseDbConnectionMiddleware($this->em->reveal());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function connectionIsClosedWhenMiddlewareIsProcessed()
|
||||
public function connectionIsClosedWhenMiddlewareIsProcessed(): void
|
||||
{
|
||||
$req = new ServerRequest();
|
||||
$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();
|
||||
$this->em->getConnection()->shouldHaveBeenCalledOnce();
|
||||
$this->conn->close()->shouldHaveBeenCalledOnce();
|
||||
$this->em->clear()->shouldHaveBeenCalledOnce();
|
||||
$handle->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function connectionIsClosedEvenIfExceptionIsThrownOnInnerMiddlewares(): void
|
||||
{
|
||||
$req = new ServerRequest();
|
||||
$expectedError = new RuntimeException();
|
||||
$this->handler->handle($req)->willThrow($expectedError)
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->em->getConnection()->shouldBeCalledOnce();
|
||||
$this->conn->close()->shouldBeCalledOnce();
|
||||
$this->em->clear()->shouldBeCalledOnce();
|
||||
$this->expectExceptionObject($expectedError);
|
||||
|
||||
$this->middleware->process($req, $this->handler->reveal());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,15 @@ class IpAddressMiddlewareFactoryTest extends TestCase
|
||||
$this->factory = new IpAddressMiddlewareFactory();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function returnedInstanceIsProperlyConfigured()
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideConfigs
|
||||
*/
|
||||
public function returnedInstanceIsProperlyConfigured(array $config, array $expectedHeadersToInspect): void
|
||||
{
|
||||
$instance = $this->factory->__invoke(new ServiceManager(), '');
|
||||
$instance = ($this->factory)(new ServiceManager(['services' => [
|
||||
'config' => $config,
|
||||
]]), '');
|
||||
|
||||
$ref = new ReflectionObject($instance);
|
||||
$checkProxyHeaders = $ref->getProperty('checkProxyHeaders');
|
||||
@@ -30,9 +35,52 @@ class IpAddressMiddlewareFactoryTest extends TestCase
|
||||
$trustedProxies->setAccessible(true);
|
||||
$attributeName = $ref->getProperty('attributeName');
|
||||
$attributeName->setAccessible(true);
|
||||
$headersToInspect = $ref->getProperty('headersToInspect');
|
||||
$headersToInspect->setAccessible(true);
|
||||
|
||||
$this->assertTrue($checkProxyHeaders->getValue($instance));
|
||||
$this->assertEquals([], $trustedProxies->getValue($instance));
|
||||
$this->assertEquals(Visitor::REMOTE_ADDRESS_ATTR, $attributeName->getValue($instance));
|
||||
$this->assertEquals($expectedHeadersToInspect, $headersToInspect->getValue($instance));
|
||||
}
|
||||
|
||||
public function provideConfigs(): iterable
|
||||
{
|
||||
$defaultHeadersToInspect = [
|
||||
'Forwarded',
|
||||
'X-Forwarded-For',
|
||||
'X-Forwarded',
|
||||
'X-Cluster-Client-Ip',
|
||||
'Client-Ip',
|
||||
];
|
||||
|
||||
yield 'no ip_address_resolution config' => [[], $defaultHeadersToInspect];
|
||||
yield 'no headers_to_inspect config' => [['ip_address_resolution' => []], $defaultHeadersToInspect];
|
||||
yield 'null headers_to_inspect' => [['ip_address_resolution' => [
|
||||
'headers_to_inspect' => null,
|
||||
]], $defaultHeadersToInspect];
|
||||
yield 'empty headers_to_inspect' => [['ip_address_resolution' => [
|
||||
'headers_to_inspect' => [],
|
||||
]], $defaultHeadersToInspect];
|
||||
yield 'some headers_to_inspect' => [['ip_address_resolution' => [
|
||||
'headers_to_inspect' => [
|
||||
'foo',
|
||||
'bar',
|
||||
'baz',
|
||||
],
|
||||
]], [
|
||||
'foo',
|
||||
'bar',
|
||||
'baz',
|
||||
]];
|
||||
yield 'some other headers_to_inspect' => [['ip_address_resolution' => [
|
||||
'headers_to_inspect' => [
|
||||
'something',
|
||||
'something_else',
|
||||
],
|
||||
]], [
|
||||
'something',
|
||||
'something_else',
|
||||
]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class TestUtils
|
||||
{
|
||||
private static $prophet;
|
||||
|
||||
public static function createReqHandlerMock(ResponseInterface $response = null, RequestInterface $request = null)
|
||||
public static function createReqHandlerMock(?ResponseInterface $response = null, ?RequestInterface $request = null)
|
||||
{
|
||||
$argument = $request ?: Argument::any();
|
||||
$delegate = static::getProphet()->prophesize(RequestHandlerInterface::class);
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
||||
use Zend\Expressive\Router\RouterInterface;
|
||||
@@ -46,7 +47,7 @@ return [
|
||||
Options\UrlShortenerOptions::class => ['config.url_shortener'],
|
||||
|
||||
Service\UrlShortener::class => ['httpClient', 'em', Options\UrlShortenerOptions::class],
|
||||
Service\VisitsTracker::class => ['em'],
|
||||
Service\VisitsTracker::class => ['em', EventDispatcherInterface::class],
|
||||
Service\ShortUrlService::class => ['em'],
|
||||
Service\VisitService::class => ['em'],
|
||||
Service\Tag\TagService::class => ['em'],
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
||||
|
||||
/** @var $metadata ClassMetadata */
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
|
||||
/** @var $metadata ClassMetadata */
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
||||
|
||||
/** @var $metadata ClassMetadata */
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
|
||||
/** @var $metadata ClassMetadata */
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
36
module/Core/config/event_dispatcher.config.php
Normal file
36
module/Core/config/event_dispatcher.config.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
|
||||
return [
|
||||
|
||||
'events' => [
|
||||
'regular' => [],
|
||||
'async' => [
|
||||
EventDispatcher\ShortUrlVisited::class => [
|
||||
EventDispatcher\LocateShortUrlVisit::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
EventDispatcher\LocateShortUrlVisit::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
EventDispatcher\LocateShortUrlVisit::class => [
|
||||
IpLocationResolverInterface::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
GeolocationDbUpdater::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -33,7 +33,7 @@ abstract class AbstractTrackingAction implements MiddlewareInterface
|
||||
UrlShortenerInterface $urlShortener,
|
||||
VisitsTrackerInterface $visitTracker,
|
||||
AppOptions $appOptions,
|
||||
LoggerInterface $logger = null
|
||||
?LoggerInterface $logger = null
|
||||
) {
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->visitTracker = $visitTracker;
|
||||
|
||||
@@ -32,7 +32,7 @@ class PreviewAction implements MiddlewareInterface
|
||||
public function __construct(
|
||||
PreviewGeneratorInterface $previewGenerator,
|
||||
UrlShortenerInterface $urlShortener,
|
||||
LoggerInterface $logger = null
|
||||
?LoggerInterface $logger = null
|
||||
) {
|
||||
$this->previewGenerator = $previewGenerator;
|
||||
$this->urlShortener = $urlShortener;
|
||||
|
||||
@@ -36,7 +36,7 @@ class QrCodeAction implements MiddlewareInterface
|
||||
public function __construct(
|
||||
RouterInterface $router,
|
||||
UrlShortenerInterface $urlShortener,
|
||||
LoggerInterface $logger = null
|
||||
?LoggerInterface $logger = null
|
||||
) {
|
||||
$this->router = $router;
|
||||
$this->urlShortener = $urlShortener;
|
||||
|
||||
@@ -25,7 +25,7 @@ class RedirectAction extends AbstractTrackingAction
|
||||
VisitsTrackerInterface $visitTracker,
|
||||
Options\AppOptions $appOptions,
|
||||
Options\NotFoundShortUrlOptions $notFoundOptions,
|
||||
LoggerInterface $logger = null
|
||||
?LoggerInterface $logger = null
|
||||
) {
|
||||
parent::__construct($urlShortener, $visitTracker, $appOptions, $logger);
|
||||
$this->notFoundOptions = $notFoundOptions;
|
||||
|
||||
@@ -30,7 +30,7 @@ class ShortUrl extends AbstractEntity
|
||||
/** @var integer|null */
|
||||
private $maxVisits;
|
||||
|
||||
public function __construct(string $longUrl, ShortUrlMeta $meta = null)
|
||||
public function __construct(string $longUrl, ?ShortUrlMeta $meta = null)
|
||||
{
|
||||
$meta = $meta ?? ShortUrlMeta::createEmpty();
|
||||
|
||||
|
||||
@@ -65,6 +65,11 @@ class Visit extends AbstractEntity implements JsonSerializable
|
||||
return $this->visitLocation ?? new UnknownVisitLocation();
|
||||
}
|
||||
|
||||
public function isLocatable(): bool
|
||||
{
|
||||
return $this->hasRemoteAddr() && $this->remoteAddr !== IpAddress::LOCALHOST;
|
||||
}
|
||||
|
||||
public function locate(VisitLocation $visitLocation): self
|
||||
{
|
||||
$this->visitLocation = $visitLocation;
|
||||
|
||||
86
module/Core/src/EventDispatcher/LocateShortUrlVisit.php
Normal file
86
module/Core/src/EventDispatcher/LocateShortUrlVisit.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class LocateShortUrlVisit
|
||||
{
|
||||
/** @var IpLocationResolverInterface */
|
||||
private $ipLocationResolver;
|
||||
/** @var EntityManagerInterface */
|
||||
private $em;
|
||||
/** @var LoggerInterface */
|
||||
private $logger;
|
||||
/** @var GeolocationDbUpdaterInterface */
|
||||
private $dbUpdater;
|
||||
|
||||
public function __construct(
|
||||
IpLocationResolverInterface $ipLocationResolver,
|
||||
EntityManagerInterface $em,
|
||||
LoggerInterface $logger,
|
||||
GeolocationDbUpdaterInterface $dbUpdater
|
||||
) {
|
||||
$this->ipLocationResolver = $ipLocationResolver;
|
||||
$this->em = $em;
|
||||
$this->logger = $logger;
|
||||
$this->dbUpdater = $dbUpdater;
|
||||
}
|
||||
|
||||
public function __invoke(ShortUrlVisited $shortUrlVisited): void
|
||||
{
|
||||
$visitId = $shortUrlVisited->visitId();
|
||||
|
||||
/** @var Visit|null $visit */
|
||||
$visit = $this->em->find(Visit::class, $visitId);
|
||||
if ($visit === null) {
|
||||
$this->logger->warning(sprintf('Tried to locate visit with id "%s", but it does not exist.', $visitId));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) {
|
||||
$this->logger->notice(sprintf('%s GeoLite2 database...', $olderDbExists ? 'Updating' : 'Downloading'));
|
||||
});
|
||||
} catch (GeolocationDbUpdateFailedException $e) {
|
||||
if (! $e->olderDbExists()) {
|
||||
$this->logger->error(
|
||||
sprintf(
|
||||
'GeoLite2 database download failed. It is not possible to locate visit with id %s. {e}',
|
||||
$visitId
|
||||
),
|
||||
['e' => $e]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->warning('GeoLite2 database update failed. Proceeding with old version. {e}', ['e' => $e]);
|
||||
}
|
||||
|
||||
try {
|
||||
$location = $visit->isLocatable()
|
||||
? $this->ipLocationResolver->resolveIpLocation($visit->getRemoteAddr())
|
||||
: Location::emptyInstance();
|
||||
} catch (WrongIpException $e) {
|
||||
$this->logger->warning(
|
||||
sprintf('Tried to locate visit with id "%s", but its address seems to be wrong. {e}', $visitId),
|
||||
['e' => $e]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$visit->locate(new VisitLocation($location));
|
||||
$this->em->flush($visit);
|
||||
}
|
||||
}
|
||||
27
module/Core/src/EventDispatcher/ShortUrlVisited.php
Normal file
27
module/Core/src/EventDispatcher/ShortUrlVisited.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
final class ShortUrlVisited implements JsonSerializable
|
||||
{
|
||||
/** @var string */
|
||||
private $visitId;
|
||||
|
||||
public function __construct(string $visitId)
|
||||
{
|
||||
$this->visitId = $visitId;
|
||||
}
|
||||
|
||||
public function visitId(): string
|
||||
{
|
||||
return $this->visitId;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return ['visitId' => $this->visitId];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user