mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-06 23:33:13 +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/database.sqlite
|
||||||
data/shlink-tests.db
|
data/shlink-tests.db
|
||||||
data/GeoLite2-City.mmdb
|
data/GeoLite2-City.mmdb
|
||||||
|
data/GeoLite2-City.mmdb.*
|
||||||
docs/swagger-ui*
|
docs/swagger-ui*
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
.phpunit.result.cache
|
.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).
|
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
|
## 1.17.0 - 2019-05-13
|
||||||
|
|
||||||
#### Added
|
#### 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):
|
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.
|
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.*
|
*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
|
## Update to new version
|
||||||
|
|
||||||
@@ -268,6 +268,9 @@ Available commands:
|
|||||||
config
|
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-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
|
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
|
||||||
short-url:delete [short-code:delete] Deletes a 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
|
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=bin/test \
|
||||||
--exclude=data/infra \
|
--exclude=data/infra \
|
||||||
--exclude=data/travis \
|
--exclude=data/travis \
|
||||||
|
--exclude=data/cache/* \
|
||||||
|
--exclude=data/log/* \
|
||||||
|
--exclude=data/locks/* \
|
||||||
|
--exclude=data/proxies/* \
|
||||||
--exclude=data/migrations_template.txt \
|
--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=**/.gitignore \
|
||||||
--exclude=CHANGELOG.md \
|
--exclude=CHANGELOG.md \
|
||||||
--exclude=composer.lock \
|
--exclude=composer.lock \
|
||||||
@@ -47,7 +53,6 @@ ${composerBin} install --no-dev --optimize-autoloader --apcu-autoloader --no-pro
|
|||||||
# Delete development files
|
# Delete development files
|
||||||
echo 'Deleting dev files...'
|
echo 'Deleting dev files...'
|
||||||
rm composer.*
|
rm composer.*
|
||||||
rm -f data/database.sqlite
|
|
||||||
|
|
||||||
# Update shlink version in config
|
# Update shlink version in config
|
||||||
sed -i "s/%SHLINK_VERSION%/${version}/g" config/autoload/app_options.global.php
|
sed -i "s/%SHLINK_VERSION%/${version}/g" config/autoload/app_options.global.php
|
||||||
|
|||||||
@@ -29,50 +29,56 @@
|
|||||||
"lstrojny/functional-php": "^1.8",
|
"lstrojny/functional-php": "^1.8",
|
||||||
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
||||||
"monolog/monolog": "^1.21",
|
"monolog/monolog": "^1.21",
|
||||||
"shlinkio/shlink-installer": "^1.1",
|
"ocramius/proxy-manager": "~2.2.2",
|
||||||
"symfony/console": "^4.2",
|
"phly/phly-event-dispatcher": "^1.0",
|
||||||
"symfony/filesystem": "^4.2",
|
"predis/predis": "^1.1",
|
||||||
"symfony/lock": "^4.2",
|
"shlinkio/shlink-installer": "^1.2.1",
|
||||||
"symfony/process": "^4.2",
|
"symfony/console": "^4.3",
|
||||||
|
"symfony/filesystem": "^4.3",
|
||||||
|
"symfony/lock": "^4.3",
|
||||||
|
"symfony/process": "^4.3",
|
||||||
"theorchard/monolog-cascade": "^0.4",
|
"theorchard/monolog-cascade": "^0.4",
|
||||||
"zendframework/zend-config": "^3.0",
|
"zendframework/zend-config": "^3.3",
|
||||||
"zendframework/zend-config-aggregator": "^1.0",
|
"zendframework/zend-config-aggregator": "^1.1",
|
||||||
"zendframework/zend-diactoros": "^2.1.1",
|
"zendframework/zend-diactoros": "^2.1.3",
|
||||||
"zendframework/zend-expressive": "^3.0",
|
"zendframework/zend-expressive": "^3.2",
|
||||||
"zendframework/zend-expressive-fastroute": "^3.0",
|
"zendframework/zend-expressive-fastroute": "^3.0",
|
||||||
"zendframework/zend-expressive-helpers": "^5.0",
|
"zendframework/zend-expressive-helpers": "^5.3",
|
||||||
"zendframework/zend-expressive-platesrenderer": "^2.0",
|
"zendframework/zend-expressive-platesrenderer": "^2.1",
|
||||||
"zendframework/zend-expressive-swoole": "^2.4",
|
"zendframework/zend-expressive-swoole": "^2.4",
|
||||||
"zendframework/zend-i18n": "^2.7",
|
"zendframework/zend-i18n": "^2.9",
|
||||||
"zendframework/zend-inputfilter": "^2.8",
|
"zendframework/zend-inputfilter": "^2.10",
|
||||||
"zendframework/zend-paginator": "^2.6",
|
"zendframework/zend-paginator": "^2.8",
|
||||||
"zendframework/zend-servicemanager": "^3.2",
|
"zendframework/zend-servicemanager": "^3.4",
|
||||||
"zendframework/zend-stdlib": "^3.0"
|
"zendframework/zend-stdlib": "^3.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"devster/ubench": "^2.0",
|
"devster/ubench": "^2.0",
|
||||||
"doctrine/data-fixtures": "^1.3",
|
"doctrine/data-fixtures": "^1.3",
|
||||||
"filp/whoops": "^2.0",
|
"eaglewu/swoole-ide-helper": "dev-master",
|
||||||
|
"filp/whoops": "^2.4",
|
||||||
"infection/infection": "^0.12.2",
|
"infection/infection": "^0.12.2",
|
||||||
"phpstan/phpstan": "^0.11.2",
|
"phpstan/phpstan": "^0.11.2",
|
||||||
"phpunit/phpcov": "^6.0",
|
"phpunit/phpcov": "^6.0",
|
||||||
"phpunit/phpunit": "^8.0",
|
"phpunit/phpunit": "^8.3",
|
||||||
"roave/security-advisories": "dev-master",
|
"roave/security-advisories": "dev-master",
|
||||||
"shlinkio/php-coding-standard": "~1.1.0",
|
"shlinkio/php-coding-standard": "~1.2.2",
|
||||||
"symfony/dotenv": "^4.2",
|
"symfony/dotenv": "^4.3",
|
||||||
"symfony/var-dumper": "^4.2",
|
"symfony/var-dumper": "^4.3",
|
||||||
"zendframework/zend-component-installer": "^2.1",
|
"zendframework/zend-component-installer": "^2.1",
|
||||||
"zendframework/zend-expressive-tooling": "^1.0"
|
"zendframework/zend-expressive-tooling": "^1.2"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
|
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
|
||||||
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
|
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
|
||||||
"Shlinkio\\Shlink\\Core\\": "module/Core/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": [
|
"files": [
|
||||||
"module/Common/functions/functions.php"
|
"module/Common/functions/functions.php",
|
||||||
|
"module/EventDispatcher/functions/functions.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
@@ -87,7 +93,8 @@
|
|||||||
"ShlinkioTest\\Shlink\\Common\\": [
|
"ShlinkioTest\\Shlink\\Common\\": [
|
||||||
"module/Common/test",
|
"module/Common/test",
|
||||||
"module/Common/test-db"
|
"module/Common/test-db"
|
||||||
]
|
],
|
||||||
|
"ShlinkioTest\\Shlink\\EventDispatcher\\": "module/EventDispatcher/test"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -110,15 +117,18 @@
|
|||||||
"test:ci": [
|
"test:ci": [
|
||||||
"@test:unit:ci",
|
"@test:unit:ci",
|
||||||
"@test:db",
|
"@test:db",
|
||||||
"@test:db:mysql",
|
|
||||||
"@test:db:postgres",
|
|
||||||
"@test:api"
|
"@test:api"
|
||||||
],
|
],
|
||||||
"test:unit": "phpdbg -qrr vendor/bin/phpunit --order-by=random --coverage-php build/coverage-unit.cov --testdox",
|
"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: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": [
|
||||||
"test:db:mysql": "DB_DRIVER=mysql composer test:db",
|
"@test:db:sqlite",
|
||||||
"test:db:postgres": "DB_DRIVER=postgres composer test:db",
|
"@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:api": "bin/test/run-api-tests.sh",
|
||||||
|
|
||||||
"test:pretty": [
|
"test:pretty": [
|
||||||
@@ -127,9 +137,9 @@
|
|||||||
],
|
],
|
||||||
"test:unit:pretty": "phpdbg -qrr vendor/bin/phpunit --coverage-html build/coverage --order-by=random",
|
"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": "infection --threads=4 --min-msi=75 --log-verbosity=default --only-covered",
|
||||||
"infect:ci": "infection --threads=4 --min-msi=70 --log-verbosity=default --only-covered --coverage=build",
|
"infect:ci": "infection --threads=4 --min-msi=75 --log-verbosity=default --only-covered --coverage=build",
|
||||||
"infect:show": "infection --threads=4 --min-msi=70 --log-verbosity=default --only-covered --show-mutations",
|
"infect:show": "infection --threads=4 --min-msi=75 --log-verbosity=default --only-covered --show-mutations",
|
||||||
"infect:test": [
|
"infect:test": [
|
||||||
"@test:unit:ci",
|
"@test:unit:ci",
|
||||||
"@infect: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: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": "<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: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: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:db:postgres": "<fg=blue;options=bold>Runs database test suites on a PostgreSQL database</>",
|
||||||
"test:api": "<fg=blue;options=bold>Runs API test suites</>",
|
"test:api": "<fg=blue;options=bold>Runs API test suites</>",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use function Shlinkio\Shlink\Common\env;
|
namespace Shlinkio\Shlink\Common;
|
||||||
|
|
||||||
return [
|
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
|
<?php
|
||||||
declare(strict_types=1);
|
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 Symfony\Component\Lock;
|
||||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
|
|
||||||
@@ -13,13 +16,28 @@ return [
|
|||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'factories' => [
|
'factories' => [
|
||||||
Lock\Store\FlockStore::class => ConfigAbstractFactory::class,
|
Lock\Store\FlockStore::class => ConfigAbstractFactory::class,
|
||||||
|
Lock\Store\RedisStore::class => ConfigAbstractFactory::class,
|
||||||
Lock\Factory::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 => [
|
ConfigAbstractFactory::class => [
|
||||||
Lock\Store\FlockStore::class => ['config.locks.locks_dir'],
|
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' => [
|
'dependencies' => [
|
||||||
'factories' => [
|
'factories' => [
|
||||||
'Logger_Shlink' => Common\Factory\LoggerFactory::class,
|
'Logger_Shlink' => Common\Logger\LoggerFactory::class,
|
||||||
'Logger_Access' => Common\Factory\LoggerFactory::class,
|
'Logger_Access' => Common\Logger\LoggerFactory::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,40 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
|
|
||||||
return [
|
$isSwoole = extension_loaded('swoole');
|
||||||
|
|
||||||
'logger' => [
|
// For swoole, send logs to standard output
|
||||||
'handlers' => [
|
$logger = $isSwoole ? [
|
||||||
'shlink_rotating_handler' => [
|
'handlers' => [
|
||||||
'level' => Logger::DEBUG,
|
'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' => [
|
'swoole-http-server' => [
|
||||||
'host' => '0.0.0.0',
|
'host' => '0.0.0.0',
|
||||||
'process-name' => 'shlink',
|
'process-name' => 'shlink',
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
'worker_num' => 16,
|
||||||
|
'task_worker_num' => 16,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ return (new ConfigAggregator\ConfigAggregator([
|
|||||||
Core\ConfigProvider::class,
|
Core\ConfigProvider::class,
|
||||||
CLI\ConfigProvider::class,
|
CLI\ConfigProvider::class,
|
||||||
Rest\ConfigProvider::class,
|
Rest\ConfigProvider::class,
|
||||||
|
EventDispatcher\ConfigProvider::class,
|
||||||
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
|
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
|
||||||
new ConfigAggregator\ZendConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'),
|
|
||||||
env('APP_ENV') === 'test'
|
env('APP_ENV') === 'test'
|
||||||
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
|
? 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', [
|
], 'data/cache/app_config.php', [
|
||||||
Core\ConfigPostProcessor::class,
|
Core\SimplifiedConfigParser::class,
|
||||||
]))->getMergedConfig();
|
]))->getMergedConfig();
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ return [
|
|||||||
'process-name' => 'shlink_test',
|
'process-name' => 'shlink_test',
|
||||||
'options' => [
|
'options' => [
|
||||||
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
|
'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 swoole port
|
||||||
EXPOSE 8080
|
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
|
# 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
|
# 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
|
until php ./vendor/bin/zend-expressive-swoole start; do sleep 1 ; done
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ services:
|
|||||||
links:
|
links:
|
||||||
- shlink_db
|
- shlink_db
|
||||||
- shlink_db_postgres
|
- shlink_db_postgres
|
||||||
|
- shlink_redis
|
||||||
|
|
||||||
shlink_swoole:
|
shlink_swoole:
|
||||||
container_name: shlink_swoole
|
container_name: shlink_swoole
|
||||||
@@ -37,6 +38,7 @@ services:
|
|||||||
links:
|
links:
|
||||||
- shlink_db
|
- shlink_db
|
||||||
- shlink_db_postgres
|
- shlink_db_postgres
|
||||||
|
- shlink_redis
|
||||||
|
|
||||||
shlink_db:
|
shlink_db:
|
||||||
container_name: shlink_db
|
container_name: shlink_db
|
||||||
@@ -62,3 +64,9 @@ services:
|
|||||||
POSTGRES_PASSWORD: root
|
POSTGRES_PASSWORD: root
|
||||||
POSTGRES_DB: shlink
|
POSTGRES_DB: shlink
|
||||||
PGDATA: /var/lib/postgresql/data/pgdata
|
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"
|
"description": "A list of tags applied to this short URL"
|
||||||
},
|
},
|
||||||
|
"meta": {
|
||||||
|
"$ref": "./ShortUrlMeta.json"
|
||||||
|
},
|
||||||
"originalUrl": {
|
"originalUrl": {
|
||||||
"deprecated": true,
|
"deprecated": true,
|
||||||
"type": "string",
|
"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": [
|
"tags": [
|
||||||
"games",
|
"games",
|
||||||
"tech"
|
"tech"
|
||||||
]
|
],
|
||||||
|
"meta": {
|
||||||
|
"validSince": "2017-01-21T00:00:00+02:00",
|
||||||
|
"validUntil": null,
|
||||||
|
"maxVisits": 100
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"shortCode": "12Kb3",
|
"shortCode": "12Kb3",
|
||||||
@@ -110,7 +115,12 @@
|
|||||||
"visitsCount": 1029,
|
"visitsCount": 1029,
|
||||||
"tags": [
|
"tags": [
|
||||||
"shlink"
|
"shlink"
|
||||||
]
|
],
|
||||||
|
"meta": {
|
||||||
|
"validSince": null,
|
||||||
|
"validUntil": null,
|
||||||
|
"maxVisits": null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"shortCode": "123bA",
|
"shortCode": "123bA",
|
||||||
@@ -118,7 +128,12 @@
|
|||||||
"longUrl": "https://www.google.com",
|
"longUrl": "https://www.google.com",
|
||||||
"dateCreated": "2015-10-01T20:34:16+02:00",
|
"dateCreated": "2015-10-01T20:34:16+02:00",
|
||||||
"visitsCount": 25,
|
"visitsCount": 25,
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"meta": {
|
||||||
|
"validSince": "2017-01-21T00:00:00+02:00",
|
||||||
|
"validUntil": null,
|
||||||
|
"maxVisits": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pagination": {
|
"pagination": {
|
||||||
@@ -227,7 +242,12 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"games",
|
"games",
|
||||||
"tech"
|
"tech"
|
||||||
]
|
],
|
||||||
|
"meta": {
|
||||||
|
"validSince": "2017-01-21T00:00:00+02:00",
|
||||||
|
"validUntil": null,
|
||||||
|
"maxVisits": 500
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,7 +64,12 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"games",
|
"games",
|
||||||
"tech"
|
"tech"
|
||||||
]
|
],
|
||||||
|
"meta": {
|
||||||
|
"validSince": "2017-01-21T00:00:00+02:00",
|
||||||
|
"validUntil": null,
|
||||||
|
"maxVisits": 100
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"text/plain": "https://doma.in/abc123"
|
"text/plain": "https://doma.in/abc123"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,12 @@
|
|||||||
"visitsCount": 1029,
|
"visitsCount": 1029,
|
||||||
"tags": [
|
"tags": [
|
||||||
"shlink"
|
"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\CreateTagCommand::NAME => Command\Tag\CreateTagCommand::class,
|
||||||
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
||||||
Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::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;
|
namespace Shlinkio\Shlink\CLI;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
use GeoIp2\Database\Reader;
|
use GeoIp2\Database\Reader;
|
||||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||||
|
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
||||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdater;
|
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdater;
|
||||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||||
use Shlinkio\Shlink\Core\Service;
|
use Shlinkio\Shlink\Core\Service;
|
||||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console as SymfonyCli;
|
||||||
use Symfony\Component\Lock;
|
use Symfony\Component\Lock\Factory as Locker;
|
||||||
|
use Symfony\Component\Process\PhpExecutableFinder;
|
||||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||||
|
|
||||||
@@ -19,7 +22,9 @@ return [
|
|||||||
|
|
||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'factories' => [
|
'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,
|
GeolocationDbUpdater::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
@@ -44,11 +49,14 @@ return [
|
|||||||
Command\Tag\CreateTagCommand::class => ConfigAbstractFactory::class,
|
Command\Tag\CreateTagCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class,
|
Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Tag\DeleteTagsCommand::class => ConfigAbstractFactory::class,
|
Command\Tag\DeleteTagsCommand::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
|
Command\Db\CreateDatabaseCommand::class => ConfigAbstractFactory::class,
|
||||||
|
Command\Db\MigrateDatabaseCommand::class => ConfigAbstractFactory::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\GenerateShortUrlCommand::class => [Service\UrlShortener::class, 'config.url_shortener.domain'],
|
||||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class],
|
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class],
|
||||||
@@ -60,7 +68,7 @@ return [
|
|||||||
Command\Visit\LocateVisitsCommand::class => [
|
Command\Visit\LocateVisitsCommand::class => [
|
||||||
Service\VisitService::class,
|
Service\VisitService::class,
|
||||||
IpLocationResolverInterface::class,
|
IpLocationResolverInterface::class,
|
||||||
Lock\Factory::class,
|
Locker::class,
|
||||||
GeolocationDbUpdater::class,
|
GeolocationDbUpdater::class,
|
||||||
],
|
],
|
||||||
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
|
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
|
||||||
@@ -73,6 +81,19 @@ return [
|
|||||||
Command\Tag\CreateTagCommand::class => [Service\Tag\TagService::class],
|
Command\Tag\CreateTagCommand::class => [Service\Tag\TagService::class],
|
||||||
Command\Tag\RenameTagCommand::class => [Service\Tag\TagService::class],
|
Command\Tag\RenameTagCommand::class => [Service\Tag\TagService::class],
|
||||||
Command\Tag\DeleteTagsCommand::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 Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\Paginator\Paginator;
|
use Zend\Paginator\Paginator;
|
||||||
|
|
||||||
|
use function array_flip;
|
||||||
|
use function array_intersect_key;
|
||||||
use function array_values;
|
use function array_values;
|
||||||
use function count;
|
use function count;
|
||||||
use function explode;
|
use function explode;
|
||||||
@@ -29,6 +31,14 @@ class ListShortUrlsCommand extends Command
|
|||||||
|
|
||||||
public const NAME = 'short-url:list';
|
public const NAME = 'short-url:list';
|
||||||
private const ALIASES = ['shortcode:list', 'short-code:list'];
|
private const ALIASES = ['shortcode:list', 'short-code:list'];
|
||||||
|
private const COLUMNS_WHITELIST = [
|
||||||
|
'shortCode',
|
||||||
|
'shortUrl',
|
||||||
|
'longUrl',
|
||||||
|
'dateCreated',
|
||||||
|
'visitsCount',
|
||||||
|
'tags',
|
||||||
|
];
|
||||||
|
|
||||||
/** @var ShortUrlServiceInterface */
|
/** @var ShortUrlServiceInterface */
|
||||||
private $shortUrlService;
|
private $shortUrlService;
|
||||||
@@ -125,8 +135,7 @@ class ListShortUrlsCommand extends Command
|
|||||||
unset($shortUrl['tags']);
|
unset($shortUrl['tags']);
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($shortUrl['originalUrl']);
|
$rows[] = array_values(array_intersect_key($shortUrl, array_flip(self::COLUMNS_WHITELIST)));
|
||||||
$rows[] = array_values($shortUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage(
|
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;
|
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\Exception\GeolocationDbUpdateFailedException;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
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\Entity\VisitLocation;
|
||||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
|
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Helper\ProgressBar;
|
use Symfony\Component\Console\Helper\ProgressBar;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Symfony\Component\Lock\Factory as Locker;
|
use Symfony\Component\Lock\Factory as Locker;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
|
|
||||||
class LocateVisitsCommand extends Command
|
class LocateVisitsCommand extends AbstractLockedCommand
|
||||||
{
|
{
|
||||||
public const NAME = 'visit:locate';
|
public const NAME = 'visit:locate';
|
||||||
public const ALIASES = ['visit:process'];
|
public const ALIASES = ['visit:process'];
|
||||||
@@ -32,8 +35,6 @@ class LocateVisitsCommand extends Command
|
|||||||
private $visitService;
|
private $visitService;
|
||||||
/** @var IpLocationResolverInterface */
|
/** @var IpLocationResolverInterface */
|
||||||
private $ipLocationResolver;
|
private $ipLocationResolver;
|
||||||
/** @var Locker */
|
|
||||||
private $locker;
|
|
||||||
/** @var GeolocationDbUpdaterInterface */
|
/** @var GeolocationDbUpdaterInterface */
|
||||||
private $dbUpdater;
|
private $dbUpdater;
|
||||||
|
|
||||||
@@ -48,10 +49,9 @@ class LocateVisitsCommand extends Command
|
|||||||
Locker $locker,
|
Locker $locker,
|
||||||
GeolocationDbUpdaterInterface $dbUpdater
|
GeolocationDbUpdaterInterface $dbUpdater
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct($locker);
|
||||||
$this->visitService = $visitService;
|
$this->visitService = $visitService;
|
||||||
$this->ipLocationResolver = $ipLocationResolver;
|
$this->ipLocationResolver = $ipLocationResolver;
|
||||||
$this->locker = $locker;
|
|
||||||
$this->dbUpdater = $dbUpdater;
|
$this->dbUpdater = $dbUpdater;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,23 +63,17 @@ class LocateVisitsCommand extends Command
|
|||||||
->setDescription('Resolves visits origin locations.');
|
->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);
|
$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 {
|
try {
|
||||||
$this->checkDbUpdate();
|
$this->checkDbUpdate();
|
||||||
|
|
||||||
$this->visitService->locateUnlocatedVisits(
|
$this->visitService->locateUnlocatedVisits(
|
||||||
[$this, 'getGeolocationDataForVisit'],
|
[$this, 'getGeolocationDataForVisit'],
|
||||||
function (VisitLocation $location) use ($output) {
|
static function (VisitLocation $location) use ($output) {
|
||||||
if (! $location->isEmpty()) {
|
if (!$location->isEmpty()) {
|
||||||
$output->writeln(
|
$output->writeln(
|
||||||
sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName())
|
sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName())
|
||||||
);
|
);
|
||||||
@@ -88,9 +82,14 @@ class LocateVisitsCommand extends Command
|
|||||||
);
|
);
|
||||||
|
|
||||||
$this->io->success('Finished processing all IPs');
|
$this->io->success('Finished processing all IPs');
|
||||||
} finally {
|
|
||||||
$lock->release();
|
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
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 */
|
/** @var bool */
|
||||||
private $olderDbExists;
|
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;
|
$this->olderDbExists = $olderDbExists;
|
||||||
parent::__construct($message, $code, $previous);
|
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(
|
return new self(
|
||||||
$olderDbExists,
|
$olderDbExists,
|
||||||
|
|||||||
@@ -4,32 +4,13 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\CLI\Factory;
|
namespace Shlinkio\Shlink\CLI\Factory;
|
||||||
|
|
||||||
use Interop\Container\ContainerInterface;
|
use Interop\Container\ContainerInterface;
|
||||||
use Interop\Container\Exception\ContainerException;
|
|
||||||
use Psr\Container\ContainerExceptionInterface;
|
|
||||||
use Psr\Container\NotFoundExceptionInterface;
|
|
||||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
use Symfony\Component\Console\Application as CliApp;
|
use Symfony\Component\Console\Application as CliApp;
|
||||||
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
|
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
|
||||||
{
|
{
|
||||||
/**
|
public function __invoke(ContainerInterface $container): CliApp
|
||||||
* 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
|
|
||||||
{
|
{
|
||||||
$config = $container->get('config')['cli'];
|
$config = $container->get('config')['cli'];
|
||||||
$appOptions = $container->get(AppOptions::class);
|
$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 Cake\Chronos\Chronos;
|
||||||
use GeoIp2\Database\Reader;
|
use GeoIp2\Database\Reader;
|
||||||
use InvalidArgumentException;
|
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||||
|
use Symfony\Component\Lock\Factory as Locker;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
||||||
{
|
{
|
||||||
|
private const LOCK_NAME = 'geolocation-db-update';
|
||||||
|
|
||||||
/** @var DbUpdaterInterface */
|
/** @var DbUpdaterInterface */
|
||||||
private $dbUpdater;
|
private $dbUpdater;
|
||||||
/** @var Reader */
|
/** @var Reader */
|
||||||
private $geoLiteDbReader;
|
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->dbUpdater = $dbUpdater;
|
||||||
$this->geoLiteDbReader = $geoLiteDbReader;
|
$this->geoLiteDbReader = $geoLiteDbReader;
|
||||||
|
$this->locker = $locker;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws GeolocationDbUpdateFailedException
|
* @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 {
|
try {
|
||||||
$meta = $this->geoLiteDbReader->metadata();
|
$this->downloadIfNeeded($mustBeUpdated, $handleProgress);
|
||||||
if ($this->buildIsTooOld($meta->__get('buildEpoch'))) {
|
} catch (Throwable $e) {
|
||||||
$this->downloadNewDb(true, $mustBeUpdated, $handleProgress);
|
throw $e;
|
||||||
}
|
} finally {
|
||||||
} catch (InvalidArgumentException $e) {
|
$lock->release();
|
||||||
// This is the exception thrown by the reader when the database file does not exist
|
|
||||||
$this->downloadNewDb(false, $mustBeUpdated, $handleProgress);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildIsTooOld(int $buildTimestamp): bool
|
/**
|
||||||
|
* @throws GeolocationDbUpdateFailedException
|
||||||
|
*/
|
||||||
|
private function downloadIfNeeded(?callable $mustBeUpdated, ?callable $handleProgress): void
|
||||||
{
|
{
|
||||||
$buildDate = Chronos::createFromTimestamp($buildTimestamp);
|
if (! $this->dbUpdater->databaseFileExists()) {
|
||||||
$now = Chronos::now();
|
$this->downloadNewDb(false, $mustBeUpdated, $handleProgress);
|
||||||
return $now->gt($buildDate->addDays(35));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = $this->geoLiteDbReader->metadata();
|
||||||
|
if ($this->buildIsTooOld($meta->__get('buildEpoch'))) {
|
||||||
|
$this->downloadNewDb(true, $mustBeUpdated, $handleProgress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws GeolocationDbUpdateFailedException
|
* @throws GeolocationDbUpdateFailedException
|
||||||
*/
|
*/
|
||||||
private function downloadNewDb(
|
private function downloadNewDb(bool $olderDbExists, ?callable $mustBeUpdated, ?callable $handleProgress): void
|
||||||
bool $olderDbExists,
|
{
|
||||||
callable $mustBeUpdated = null,
|
|
||||||
callable $handleProgress = null
|
|
||||||
): void {
|
|
||||||
if ($mustBeUpdated !== null) {
|
if ($mustBeUpdated !== null) {
|
||||||
$mustBeUpdated($olderDbExists);
|
$mustBeUpdated($olderDbExists);
|
||||||
}
|
}
|
||||||
@@ -64,4 +77,11 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||||||
throw GeolocationDbUpdateFailedException::create($olderDbExists, $e);
|
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
|
* @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\Application;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
use const PHP_EOL;
|
|
||||||
|
|
||||||
use function array_pop;
|
use function array_pop;
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
|
|
||||||
|
use const PHP_EOL;
|
||||||
|
|
||||||
class DeleteShortUrlCommandTest extends TestCase
|
class DeleteShortUrlCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
/** @var CommandTester */
|
/** @var CommandTester */
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
|
|
||||||
$this->locker = $this->prophesize(Lock\Factory::class);
|
$this->locker = $this->prophesize(Lock\Factory::class);
|
||||||
$this->lock = $this->prophesize(Lock\LockInterface::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->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(
|
$command = new LocateVisitsCommand(
|
||||||
$this->visitService->reveal(),
|
$this->visitService->reveal(),
|
||||||
@@ -162,9 +162,9 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @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 () {
|
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function () {
|
||||||
});
|
});
|
||||||
@@ -174,7 +174,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString(
|
$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
|
$output
|
||||||
);
|
);
|
||||||
$locateVisits->shouldNotHaveBeenCalled();
|
$locateVisits->shouldNotHaveBeenCalled();
|
||||||
|
|||||||
@@ -25,14 +25,7 @@ class ApplicationFactoryTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function serviceIsCreated()
|
public function allCommandsWhichAreServicesAreAdded(): void
|
||||||
{
|
|
||||||
$instance = ($this->factory)($this->createServiceManager(), '');
|
|
||||||
$this->assertInstanceOf(Application::class, $instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @test */
|
|
||||||
public function allCommandsWhichAreServicesAreAdded()
|
|
||||||
{
|
{
|
||||||
$sm = $this->createServiceManager([
|
$sm = $this->createServiceManager([
|
||||||
'commands' => [
|
'commands' => [
|
||||||
@@ -45,8 +38,7 @@ class ApplicationFactoryTest extends TestCase
|
|||||||
$sm->setService('bar', $this->createCommandMock('bar')->reveal());
|
$sm->setService('bar', $this->createCommandMock('bar')->reveal());
|
||||||
|
|
||||||
/** @var Application $instance */
|
/** @var Application $instance */
|
||||||
$instance = ($this->factory)($sm, '');
|
$instance = ($this->factory)($sm);
|
||||||
$this->assertInstanceOf(Application::class, $instance);
|
|
||||||
|
|
||||||
$this->assertTrue($instance->has('foo'));
|
$this->assertTrue($instance->has('foo'));
|
||||||
$this->assertTrue($instance->has('bar'));
|
$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 Cake\Chronos\Chronos;
|
||||||
use GeoIp2\Database\Reader;
|
use GeoIp2\Database\Reader;
|
||||||
use InvalidArgumentException;
|
|
||||||
use MaxMind\Db\Reader\Metadata;
|
use MaxMind\Db\Reader\Metadata;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||||
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||||
|
use Symfony\Component\Lock;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
use function Functional\map;
|
use function Functional\map;
|
||||||
@@ -26,15 +27,27 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
private $dbUpdater;
|
private $dbUpdater;
|
||||||
/** @var ObjectProphecy */
|
/** @var ObjectProphecy */
|
||||||
private $geoLiteDbReader;
|
private $geoLiteDbReader;
|
||||||
|
/** @var ObjectProphecy */
|
||||||
|
private $locker;
|
||||||
|
/** @var ObjectProphecy */
|
||||||
|
private $lock;
|
||||||
|
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
|
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
|
||||||
$this->geoLiteDbReader = $this->prophesize(Reader::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->geolocationDbUpdater = new GeolocationDbUpdater(
|
||||||
$this->dbUpdater->reveal(),
|
$this->dbUpdater->reveal(),
|
||||||
$this->geoLiteDbReader->reveal()
|
$this->geoLiteDbReader->reveal(),
|
||||||
|
$this->locker->reveal()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +57,10 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
$mustBeUpdated = function () {
|
$mustBeUpdated = function () {
|
||||||
$this->assertTrue(true);
|
$this->assertTrue(true);
|
||||||
};
|
};
|
||||||
$getMeta = $this->geoLiteDbReader->metadata()->willThrow(InvalidArgumentException::class);
|
|
||||||
$prev = new RuntimeException('');
|
$prev = new RuntimeException('');
|
||||||
|
|
||||||
|
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(false);
|
||||||
|
$getMeta = $this->geoLiteDbReader->metadata();
|
||||||
$download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev);
|
$download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -58,7 +73,8 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
$this->assertFalse($e->olderDbExists());
|
$this->assertFalse($e->olderDbExists());
|
||||||
}
|
}
|
||||||
|
|
||||||
$getMeta->shouldHaveBeenCalledOnce();
|
$fileExists->shouldHaveBeenCalledOnce();
|
||||||
|
$getMeta->shouldNotHaveBeenCalled();
|
||||||
$download->shouldHaveBeenCalledOnce();
|
$download->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +84,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
*/
|
*/
|
||||||
public function exceptionIsThrownWhenOlderDbIsTooOldAndDownloadFails(int $days): void
|
public function exceptionIsThrownWhenOlderDbIsTooOldAndDownloadFails(int $days): void
|
||||||
{
|
{
|
||||||
|
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(true);
|
||||||
$getMeta = $this->geoLiteDbReader->metadata()->willReturn(new Metadata([
|
$getMeta = $this->geoLiteDbReader->metadata()->willReturn(new Metadata([
|
||||||
'binary_format_major_version' => '',
|
'binary_format_major_version' => '',
|
||||||
'binary_format_minor_version' => '',
|
'binary_format_minor_version' => '',
|
||||||
@@ -92,6 +109,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
$this->assertTrue($e->olderDbExists());
|
$this->assertTrue($e->olderDbExists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$fileExists->shouldHaveBeenCalledOnce();
|
||||||
$getMeta->shouldHaveBeenCalledOnce();
|
$getMeta->shouldHaveBeenCalledOnce();
|
||||||
$download->shouldHaveBeenCalledOnce();
|
$download->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
@@ -110,6 +128,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
*/
|
*/
|
||||||
public function databaseIsNotUpdatedIfItIsYoungerThanOneWeek(int $days): void
|
public function databaseIsNotUpdatedIfItIsYoungerThanOneWeek(int $days): void
|
||||||
{
|
{
|
||||||
|
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(true);
|
||||||
$getMeta = $this->geoLiteDbReader->metadata()->willReturn(new Metadata([
|
$getMeta = $this->geoLiteDbReader->metadata()->willReturn(new Metadata([
|
||||||
'binary_format_major_version' => '',
|
'binary_format_major_version' => '',
|
||||||
'binary_format_minor_version' => '',
|
'binary_format_minor_version' => '',
|
||||||
@@ -126,6 +145,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
|
|
||||||
$this->geolocationDbUpdater->checkDbUpdate();
|
$this->geolocationDbUpdater->checkDbUpdate();
|
||||||
|
|
||||||
|
$fileExists->shouldHaveBeenCalledOnce();
|
||||||
$getMeta->shouldHaveBeenCalledOnce();
|
$getMeta->shouldHaveBeenCalledOnce();
|
||||||
$download->shouldNotHaveBeenCalled();
|
$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;
|
namespace Shlinkio\Shlink\Common;
|
||||||
|
|
||||||
use Doctrine\Common\Cache\Cache;
|
|
||||||
use Doctrine\ORM\EntityManager;
|
|
||||||
use GeoIp2\Database\Reader;
|
use GeoIp2\Database\Reader;
|
||||||
use GuzzleHttp\Client as GuzzleClient;
|
use GuzzleHttp\Client as GuzzleClient;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
@@ -20,9 +18,7 @@ return [
|
|||||||
|
|
||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'factories' => [
|
'factories' => [
|
||||||
EntityManager::class => Factory\EntityManagerFactory::class,
|
|
||||||
GuzzleClient::class => InvokableFactory::class,
|
GuzzleClient::class => InvokableFactory::class,
|
||||||
Cache::class => Factory\CacheFactory::class,
|
|
||||||
Filesystem::class => InvokableFactory::class,
|
Filesystem::class => InvokableFactory::class,
|
||||||
Reader::class => ConfigAbstractFactory::class,
|
Reader::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
@@ -45,7 +41,6 @@ return [
|
|||||||
Service\PreviewGenerator::class => ConfigAbstractFactory::class,
|
Service\PreviewGenerator::class => ConfigAbstractFactory::class,
|
||||||
],
|
],
|
||||||
'aliases' => [
|
'aliases' => [
|
||||||
'em' => EntityManager::class,
|
|
||||||
'httpClient' => GuzzleClient::class,
|
'httpClient' => GuzzleClient::class,
|
||||||
'translator' => Translator::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;
|
namespace Shlinkio\Shlink\Common;
|
||||||
|
|
||||||
use const JSON_ERROR_NONE;
|
|
||||||
|
|
||||||
use function getenv;
|
use function getenv;
|
||||||
use function json_decode as spl_json_decode;
|
use function json_decode as spl_json_decode;
|
||||||
use function json_last_error;
|
use function json_last_error;
|
||||||
@@ -13,6 +11,8 @@ use function sprintf;
|
|||||||
use function strtolower;
|
use function strtolower;
|
||||||
use function trim;
|
use function trim;
|
||||||
|
|
||||||
|
use const JSON_ERROR_NONE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the value of an environment variable. Supports boolean, empty and null.
|
* Gets the value of an environment variable. Supports boolean, empty and null.
|
||||||
* This is basically Laravel's env helper
|
* This is basically Laravel's env helper
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\Common\Factory;
|
namespace Shlinkio\Shlink\Common\Cache;
|
||||||
|
|
||||||
use Doctrine\Common\Cache;
|
use Doctrine\Common\Cache;
|
||||||
use Interop\Container\ContainerInterface;
|
use Interop\Container\ContainerInterface;
|
||||||
@@ -12,8 +12,10 @@ use function Shlinkio\Shlink\Common\env;
|
|||||||
|
|
||||||
class CacheFactory implements FactoryInterface
|
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);
|
$appOptions = $container->get(AppOptions::class);
|
||||||
$adapter = env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
|
$adapter = env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
|
||||||
$adapter->setNamespace((string) $appOptions);
|
$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
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\Common\Factory;
|
namespace Shlinkio\Shlink\Common\Doctrine;
|
||||||
|
|
||||||
use Doctrine\Common\Cache\ArrayCache;
|
use Doctrine\Common\Cache\ArrayCache;
|
||||||
use Doctrine\Common\Cache\Cache;
|
use Doctrine\Common\Cache\Cache;
|
||||||
@@ -11,36 +11,42 @@ use Doctrine\DBAL\Types\Type;
|
|||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
use Doctrine\ORM\ORMException;
|
use Doctrine\ORM\ORMException;
|
||||||
use Doctrine\ORM\Tools\Setup;
|
use Doctrine\ORM\Tools\Setup;
|
||||||
use Interop\Container\ContainerInterface;
|
use Psr\Container\ContainerInterface;
|
||||||
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
|
||||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
|
||||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
|
||||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
|
||||||
|
|
||||||
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 ORMException
|
||||||
* @throws DBALException
|
* @throws DBALException
|
||||||
*/
|
*/
|
||||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
public function __invoke(ContainerInterface $container): EntityManager
|
||||||
{
|
{
|
||||||
$globalConfig = $container->get('config');
|
$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();
|
$cache = $container->has(Cache::class) ? $container->get(Cache::class) : new ArrayCache();
|
||||||
$emConfig = $globalConfig['entity_manager'] ?? [];
|
$emConfig = $globalConfig['entity_manager'] ?? [];
|
||||||
$connectionConfig = $emConfig['connection'] ?? [];
|
$connectionConfig = $emConfig['connection'] ?? [];
|
||||||
$ormConfig = $emConfig['orm'] ?? [];
|
$ormConfig = $emConfig['orm'] ?? [];
|
||||||
|
|
||||||
if (! Type::hasType(ChronosDateTimeType::CHRONOS_DATETIME)) {
|
$this->registerTypes($ormConfig);
|
||||||
Type::addType(ChronosDateTimeType::CHRONOS_DATETIME, ChronosDateTimeType::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = Setup::createConfiguration($isDevMode, $ormConfig['proxies_dir'] ?? null, $cache);
|
$config = Setup::createConfiguration($isDevMode, $ormConfig['proxies_dir'] ?? null, $cache);
|
||||||
$config->setMetadataDriverImpl(new PHPDriver($ormConfig['entities_mappings'] ?? []));
|
$config->setMetadataDriverImpl(new PHPDriver($ormConfig['entities_mappings'] ?? []));
|
||||||
|
|
||||||
return EntityManager::create($connectionConfig, $config);
|
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;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
public function setId(string $id): self
|
public function setId(string $id): self
|
||||||
{
|
{
|
||||||
$this->id = $id;
|
$this->id = $id;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use function sprintf;
|
|||||||
|
|
||||||
class WrongIpException extends RuntimeException
|
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);
|
return new self(sprintf('Provided IP "%s" is invalid', $ipAddress), 0, $prev);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class DottedAccessConfigAbstractFactory implements AbstractFactoryInterface
|
|||||||
* creating a service.
|
* creating a service.
|
||||||
* @throws ContainerException if any other error occurs
|
* @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);
|
$parts = explode('.', $requestedName);
|
||||||
$serviceName = array_shift($parts);
|
$serviceName = array_shift($parts);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class EmptyResponseImplicitOptionsMiddlewareFactory implements FactoryInterface
|
|||||||
* creating a service.
|
* creating a service.
|
||||||
* @throws ContainerException if any other error occurs
|
* @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 ImplicitOptionsMiddleware(function () {
|
||||||
return new EmptyResponse();
|
return new EmptyResponse();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class TranslatorFactory implements FactoryInterface
|
|||||||
* creating a service.
|
* creating a service.
|
||||||
* @throws ContainerException if any other error occurs
|
* @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');
|
$config = $container->get('config');
|
||||||
return Translator::factory($config['translator'] ?? []);
|
return Translator::factory($config['translator'] ?? []);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ImageBuilderFactory implements FactoryInterface
|
|||||||
* creating a service.
|
* creating a service.
|
||||||
* @throws ContainerException if any other error occurs
|
* @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' => [
|
return new ImageBuilder($container, ['factories' => [
|
||||||
Image::class => ImageFactory::class,
|
Image::class => ImageFactory::class,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ImageFactory implements FactoryInterface
|
|||||||
* creating a service.
|
* creating a service.
|
||||||
* @throws ContainerException if any other error occurs
|
* @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'];
|
$config = $container->get('config')['wkhtmltopdf'];
|
||||||
$image = new Image($config['images'] ?? null);
|
$image = new Image($config['images'] ?? null);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class DbUpdater implements DbUpdaterInterface
|
|||||||
/**
|
/**
|
||||||
* @throws RuntimeException
|
* @throws RuntimeException
|
||||||
*/
|
*/
|
||||||
public function downloadFreshCopy(callable $handleProgress = null): void
|
public function downloadFreshCopy(?callable $handleProgress = null): void
|
||||||
{
|
{
|
||||||
$tempDir = $this->options->getTempDir();
|
$tempDir = $this->options->getTempDir();
|
||||||
$compressedFile = sprintf('%s/%s', $tempDir, self::DB_COMPRESSED_FILE);
|
$compressedFile = sprintf('%s/%s', $tempDir, self::DB_COMPRESSED_FILE);
|
||||||
@@ -48,7 +48,7 @@ class DbUpdater implements DbUpdaterInterface
|
|||||||
$this->deleteTempFiles([$compressedFile, $tempFullPath]);
|
$this->deleteTempFiles([$compressedFile, $tempFullPath]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function downloadDbFile(string $dest, callable $handleProgress = null): void
|
private function downloadDbFile(string $dest, ?callable $handleProgress = null): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->httpClient->request(RequestMethod::METHOD_GET, $this->options->getDownloadFrom(), [
|
$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
|
// 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
|
interface DbUpdaterInterface
|
||||||
{
|
{
|
||||||
|
public function databaseFileExists(): bool;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws RuntimeException
|
* @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
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\Common\Factory;
|
namespace Shlinkio\Shlink\Common\Logger;
|
||||||
|
|
||||||
use Cascade\Cascade;
|
use Cascade\Cascade;
|
||||||
use Interop\Container\ContainerInterface;
|
use Interop\Container\ContainerInterface;
|
||||||
@@ -27,7 +27,7 @@ class LoggerFactory implements FactoryInterface
|
|||||||
* creating a service.
|
* creating a service.
|
||||||
* @throws ContainerException if any other error occurs
|
* @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') : [];
|
$config = $container->has('config') ? $container->get('config') : [];
|
||||||
Cascade::fileConfig($config['logger'] ?? ['loggers' => []]);
|
Cascade::fileConfig($config['logger'] ?? ['loggers' => []]);
|
||||||
@@ -3,11 +3,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Common\Logger\Processor;
|
namespace Shlinkio\Shlink\Common\Logger\Processor;
|
||||||
|
|
||||||
use const PHP_EOL;
|
|
||||||
|
|
||||||
use function str_replace;
|
use function str_replace;
|
||||||
use function strpos;
|
use function strpos;
|
||||||
|
|
||||||
|
use const PHP_EOL;
|
||||||
|
|
||||||
final class ExceptionWithNewLineProcessor
|
final class ExceptionWithNewLineProcessor
|
||||||
{
|
{
|
||||||
private const EXCEPTION_PLACEHOLDER = '{e}';
|
private const EXCEPTION_PLACEHOLDER = '{e}';
|
||||||
|
|||||||
@@ -19,16 +19,13 @@ class CloseDbConnectionMiddleware implements MiddlewareInterface
|
|||||||
$this->em = $em;
|
$this->em = $em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Process an incoming server request and return a response, optionally delegating
|
|
||||||
* response creation to a handler.
|
|
||||||
*/
|
|
||||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
{
|
{
|
||||||
$handledRequest = $handler->handle($request);
|
try {
|
||||||
$this->em->getConnection()->close();
|
return $handler->handle($request);
|
||||||
$this->em->clear();
|
} finally {
|
||||||
|
$this->em->getConnection()->close();
|
||||||
return $handledRequest;
|
$this->em->clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ class IpAddressMiddlewareFactory implements FactoryInterface
|
|||||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||||
* @throws ServiceNotCreatedException if an exception is raised when creating a 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
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function findList(
|
public function findList(
|
||||||
int $limit = null,
|
?int $limit = null,
|
||||||
int $offset = null,
|
?int $offset = null,
|
||||||
string $searchTerm = null,
|
?string $searchTerm = null,
|
||||||
array $tags = [],
|
array $tags = [],
|
||||||
$orderBy = null
|
$orderBy = null
|
||||||
): array;
|
): array;
|
||||||
@@ -30,5 +30,5 @@ interface PaginableRepositoryInterface
|
|||||||
* @param array $tags
|
* @param array $tags
|
||||||
* @return int
|
* @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
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace ShlinkioTest\Shlink\Common\Factory;
|
namespace ShlinkioTest\Shlink\Common\Cache;
|
||||||
|
|
||||||
use Doctrine\Common\Cache\ApcuCache;
|
use Doctrine\Common\Cache\ApcuCache;
|
||||||
use Doctrine\Common\Cache\ArrayCache;
|
use Doctrine\Common\Cache\ArrayCache;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\Common\Factory\CacheFactory;
|
use Shlinkio\Shlink\Common\Cache\CacheFactory;
|
||||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
use Zend\ServiceManager\ServiceManager;
|
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
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace ShlinkioTest\Shlink\Common\Factory;
|
namespace ShlinkioTest\Shlink\Common\Doctrine;
|
||||||
|
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
use PHPUnit\Framework\TestCase;
|
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;
|
use Zend\ServiceManager\ServiceManager;
|
||||||
|
|
||||||
class EntityManagerFactoryTest extends TestCase
|
class EntityManagerFactoryTest extends TestCase
|
||||||
@@ -19,12 +20,17 @@ class EntityManagerFactoryTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function serviceIsCreated()
|
public function serviceIsCreated(): void
|
||||||
{
|
{
|
||||||
$sm = new ServiceManager(['services' => [
|
$sm = new ServiceManager(['services' => [
|
||||||
'config' => [
|
'config' => [
|
||||||
'debug' => true,
|
'debug' => true,
|
||||||
'entity_manager' => [
|
'entity_manager' => [
|
||||||
|
'orm' => [
|
||||||
|
'types' => [
|
||||||
|
ChronosDateTimeType::CHRONOS_DATETIME => ChronosDateTimeType::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
'connection' => [
|
'connection' => [
|
||||||
'driver' => 'pdo_sqlite',
|
'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);
|
$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->filesystem = $this->prophesize(Filesystem::class);
|
||||||
$this->options = new GeoLite2Options([
|
$this->options = new GeoLite2Options([
|
||||||
'temp_dir' => __DIR__ . '/../../../test-resources',
|
'temp_dir' => __DIR__ . '/../../../test-resources',
|
||||||
'db_location' => '',
|
'db_location' => 'db_location',
|
||||||
'download_from' => '',
|
'download_from' => '',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -110,4 +110,23 @@ class DbUpdaterTest extends TestCase
|
|||||||
$copy->shouldHaveBeenCalledOnce();
|
$copy->shouldHaveBeenCalledOnce();
|
||||||
$remove->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
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace ShlinkioTest\Shlink\Common\Factory;
|
namespace ShlinkioTest\Shlink\Common\Logger;
|
||||||
|
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Shlinkio\Shlink\Common\Factory\LoggerFactory;
|
use Shlinkio\Shlink\Common\Logger\LoggerFactory;
|
||||||
use Zend\ServiceManager\ServiceManager;
|
use Zend\ServiceManager\ServiceManager;
|
||||||
|
|
||||||
class LoggerFactoryTest extends TestCase
|
class LoggerFactoryTest extends TestCase
|
||||||
@@ -7,11 +7,11 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use Shlinkio\Shlink\Common\Logger\Processor\ExceptionWithNewLineProcessor;
|
use Shlinkio\Shlink\Common\Logger\Processor\ExceptionWithNewLineProcessor;
|
||||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||||
|
|
||||||
use const PHP_EOL;
|
|
||||||
|
|
||||||
use function Functional\map;
|
use function Functional\map;
|
||||||
use function range;
|
use function range;
|
||||||
|
|
||||||
|
use const PHP_EOL;
|
||||||
|
|
||||||
class ExceptionWithNewLineProcessorTest extends TestCase
|
class ExceptionWithNewLineProcessorTest extends TestCase
|
||||||
{
|
{
|
||||||
use StringUtilsTrait;
|
use StringUtilsTrait;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Doctrine\ORM\EntityManagerInterface;
|
|||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use RuntimeException;
|
||||||
use Shlinkio\Shlink\Common\Middleware\CloseDbConnectionMiddleware;
|
use Shlinkio\Shlink\Common\Middleware\CloseDbConnectionMiddleware;
|
||||||
use Zend\Diactoros\Response;
|
use Zend\Diactoros\Response;
|
||||||
use Zend\Diactoros\ServerRequest;
|
use Zend\Diactoros\ServerRequest;
|
||||||
@@ -20,35 +21,52 @@ class CloseDbConnectionMiddlewareTest extends TestCase
|
|||||||
private $handler;
|
private $handler;
|
||||||
/** @var ObjectProphecy */
|
/** @var ObjectProphecy */
|
||||||
private $em;
|
private $em;
|
||||||
|
/** @var ObjectProphecy */
|
||||||
|
private $conn;
|
||||||
|
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
$this->handler = $this->prophesize(RequestHandlerInterface::class);
|
$this->handler = $this->prophesize(RequestHandlerInterface::class);
|
||||||
$this->em = $this->prophesize(EntityManagerInterface::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());
|
$this->middleware = new CloseDbConnectionMiddleware($this->em->reveal());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function connectionIsClosedWhenMiddlewareIsProcessed()
|
public function connectionIsClosedWhenMiddlewareIsProcessed(): void
|
||||||
{
|
{
|
||||||
$req = new ServerRequest();
|
$req = new ServerRequest();
|
||||||
$resp = new Response();
|
$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);
|
$handle = $this->handler->handle($req)->willReturn($resp);
|
||||||
|
|
||||||
$result = $this->middleware->process($req, $this->handler->reveal());
|
$result = $this->middleware->process($req, $this->handler->reveal());
|
||||||
|
|
||||||
$this->assertSame($result, $resp);
|
$this->assertSame($result, $resp);
|
||||||
$getConn->shouldHaveBeenCalledOnce();
|
$this->em->getConnection()->shouldHaveBeenCalledOnce();
|
||||||
$closeConn->shouldHaveBeenCalledOnce();
|
$this->conn->close()->shouldHaveBeenCalledOnce();
|
||||||
$clear->shouldHaveBeenCalledOnce();
|
$this->em->clear()->shouldHaveBeenCalledOnce();
|
||||||
$handle->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();
|
$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);
|
$ref = new ReflectionObject($instance);
|
||||||
$checkProxyHeaders = $ref->getProperty('checkProxyHeaders');
|
$checkProxyHeaders = $ref->getProperty('checkProxyHeaders');
|
||||||
@@ -30,9 +35,52 @@ class IpAddressMiddlewareFactoryTest extends TestCase
|
|||||||
$trustedProxies->setAccessible(true);
|
$trustedProxies->setAccessible(true);
|
||||||
$attributeName = $ref->getProperty('attributeName');
|
$attributeName = $ref->getProperty('attributeName');
|
||||||
$attributeName->setAccessible(true);
|
$attributeName->setAccessible(true);
|
||||||
|
$headersToInspect = $ref->getProperty('headersToInspect');
|
||||||
|
$headersToInspect->setAccessible(true);
|
||||||
|
|
||||||
$this->assertTrue($checkProxyHeaders->getValue($instance));
|
$this->assertTrue($checkProxyHeaders->getValue($instance));
|
||||||
$this->assertEquals([], $trustedProxies->getValue($instance));
|
$this->assertEquals([], $trustedProxies->getValue($instance));
|
||||||
$this->assertEquals(Visitor::REMOTE_ADDRESS_ATTR, $attributeName->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;
|
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();
|
$argument = $request ?: Argument::any();
|
||||||
$delegate = static::getProphet()->prophesize(RequestHandlerInterface::class);
|
$delegate = static::getProphet()->prophesize(RequestHandlerInterface::class);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\Core;
|
namespace Shlinkio\Shlink\Core;
|
||||||
|
|
||||||
use Doctrine\Common\Cache\Cache;
|
use Doctrine\Common\Cache\Cache;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||||
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
||||||
use Zend\Expressive\Router\RouterInterface;
|
use Zend\Expressive\Router\RouterInterface;
|
||||||
@@ -46,7 +47,7 @@ return [
|
|||||||
Options\UrlShortenerOptions::class => ['config.url_shortener'],
|
Options\UrlShortenerOptions::class => ['config.url_shortener'],
|
||||||
|
|
||||||
Service\UrlShortener::class => ['httpClient', 'em', Options\UrlShortenerOptions::class],
|
Service\UrlShortener::class => ['httpClient', 'em', Options\UrlShortenerOptions::class],
|
||||||
Service\VisitsTracker::class => ['em'],
|
Service\VisitsTracker::class => ['em', EventDispatcherInterface::class],
|
||||||
Service\ShortUrlService::class => ['em'],
|
Service\ShortUrlService::class => ['em'],
|
||||||
Service\VisitService::class => ['em'],
|
Service\VisitService::class => ['em'],
|
||||||
Service\Tag\TagService::class => ['em'],
|
Service\Tag\TagService::class => ['em'],
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace Shlinkio\Shlink\Core;
|
|||||||
|
|
||||||
use Doctrine\DBAL\Types\Type;
|
use Doctrine\DBAL\Types\Type;
|
||||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
|
||||||
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
||||||
|
|
||||||
/** @var $metadata ClassMetadata */
|
/** @var $metadata ClassMetadata */
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace Shlinkio\Shlink\Core;
|
|||||||
|
|
||||||
use Doctrine\DBAL\Types\Type;
|
use Doctrine\DBAL\Types\Type;
|
||||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
|
||||||
|
|
||||||
/** @var $metadata ClassMetadata */
|
/** @var $metadata ClassMetadata */
|
||||||
$builder = new ClassMetadataBuilder($metadata);
|
$builder = new ClassMetadataBuilder($metadata);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace Shlinkio\Shlink\Core;
|
|||||||
|
|
||||||
use Doctrine\DBAL\Types\Type;
|
use Doctrine\DBAL\Types\Type;
|
||||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
|
||||||
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
||||||
|
|
||||||
/** @var $metadata ClassMetadata */
|
/** @var $metadata ClassMetadata */
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace Shlinkio\Shlink\Core;
|
|||||||
|
|
||||||
use Doctrine\DBAL\Types\Type;
|
use Doctrine\DBAL\Types\Type;
|
||||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
|
||||||
|
|
||||||
/** @var $metadata ClassMetadata */
|
/** @var $metadata ClassMetadata */
|
||||||
$builder = new ClassMetadataBuilder($metadata);
|
$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,
|
UrlShortenerInterface $urlShortener,
|
||||||
VisitsTrackerInterface $visitTracker,
|
VisitsTrackerInterface $visitTracker,
|
||||||
AppOptions $appOptions,
|
AppOptions $appOptions,
|
||||||
LoggerInterface $logger = null
|
?LoggerInterface $logger = null
|
||||||
) {
|
) {
|
||||||
$this->urlShortener = $urlShortener;
|
$this->urlShortener = $urlShortener;
|
||||||
$this->visitTracker = $visitTracker;
|
$this->visitTracker = $visitTracker;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class PreviewAction implements MiddlewareInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
PreviewGeneratorInterface $previewGenerator,
|
PreviewGeneratorInterface $previewGenerator,
|
||||||
UrlShortenerInterface $urlShortener,
|
UrlShortenerInterface $urlShortener,
|
||||||
LoggerInterface $logger = null
|
?LoggerInterface $logger = null
|
||||||
) {
|
) {
|
||||||
$this->previewGenerator = $previewGenerator;
|
$this->previewGenerator = $previewGenerator;
|
||||||
$this->urlShortener = $urlShortener;
|
$this->urlShortener = $urlShortener;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class QrCodeAction implements MiddlewareInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
RouterInterface $router,
|
RouterInterface $router,
|
||||||
UrlShortenerInterface $urlShortener,
|
UrlShortenerInterface $urlShortener,
|
||||||
LoggerInterface $logger = null
|
?LoggerInterface $logger = null
|
||||||
) {
|
) {
|
||||||
$this->router = $router;
|
$this->router = $router;
|
||||||
$this->urlShortener = $urlShortener;
|
$this->urlShortener = $urlShortener;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class RedirectAction extends AbstractTrackingAction
|
|||||||
VisitsTrackerInterface $visitTracker,
|
VisitsTrackerInterface $visitTracker,
|
||||||
Options\AppOptions $appOptions,
|
Options\AppOptions $appOptions,
|
||||||
Options\NotFoundShortUrlOptions $notFoundOptions,
|
Options\NotFoundShortUrlOptions $notFoundOptions,
|
||||||
LoggerInterface $logger = null
|
?LoggerInterface $logger = null
|
||||||
) {
|
) {
|
||||||
parent::__construct($urlShortener, $visitTracker, $appOptions, $logger);
|
parent::__construct($urlShortener, $visitTracker, $appOptions, $logger);
|
||||||
$this->notFoundOptions = $notFoundOptions;
|
$this->notFoundOptions = $notFoundOptions;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class ShortUrl extends AbstractEntity
|
|||||||
/** @var integer|null */
|
/** @var integer|null */
|
||||||
private $maxVisits;
|
private $maxVisits;
|
||||||
|
|
||||||
public function __construct(string $longUrl, ShortUrlMeta $meta = null)
|
public function __construct(string $longUrl, ?ShortUrlMeta $meta = null)
|
||||||
{
|
{
|
||||||
$meta = $meta ?? ShortUrlMeta::createEmpty();
|
$meta = $meta ?? ShortUrlMeta::createEmpty();
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
return $this->visitLocation ?? new UnknownVisitLocation();
|
return $this->visitLocation ?? new UnknownVisitLocation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isLocatable(): bool
|
||||||
|
{
|
||||||
|
return $this->hasRemoteAddr() && $this->remoteAddr !== IpAddress::LOCALHOST;
|
||||||
|
}
|
||||||
|
|
||||||
public function locate(VisitLocation $visitLocation): self
|
public function locate(VisitLocation $visitLocation): self
|
||||||
{
|
{
|
||||||
$this->visitLocation = $visitLocation;
|
$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