mirror of
https://github.com/shlinkio/shlink.git
synced 2026-02-28 12:13:13 +08:00
Compare commits
326 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b15cd6d51 | ||
|
|
00169a5729 | ||
|
|
94702791d9 | ||
|
|
447cccacdf | ||
|
|
0413399102 | ||
|
|
9afc7876c4 | ||
|
|
187c17319a | ||
|
|
7310ecd886 | ||
|
|
620cd92d11 | ||
|
|
f9658c8da1 | ||
|
|
613b1d3045 | ||
|
|
d39711ec51 | ||
|
|
69dcab96f8 | ||
|
|
d76c96ad41 | ||
|
|
133efff2cd | ||
|
|
c10f0db170 | ||
|
|
037cd8a389 | ||
|
|
1d24750f43 | ||
|
|
b52ceaff9a | ||
|
|
6b0b52853c | ||
|
|
64d7ac7093 | ||
|
|
b9ba1246d4 | ||
|
|
7f9dc10f6a | ||
|
|
a1afc90150 | ||
|
|
df94c68e2e | ||
|
|
65ea1e00a6 | ||
|
|
5bccdded8a | ||
|
|
8917ed5c2e | ||
|
|
fabc752398 | ||
|
|
38d8086516 | ||
|
|
ae0ff5f23c | ||
|
|
7c659699f3 | ||
|
|
9e6cdcb838 | ||
|
|
7e2f755dfd | ||
|
|
ce2ed237c7 | ||
|
|
626caa4afa | ||
|
|
f4a7712ded | ||
|
|
bab6a3951e | ||
|
|
f49d98f2ea | ||
|
|
1312ea61f4 | ||
|
|
8d90661d0a | ||
|
|
b6b2530cb6 | ||
|
|
e4f66b7ce6 | ||
|
|
4b52c92e97 | ||
|
|
76c42bc17c | ||
|
|
c4f8da5f02 | ||
|
|
80bdeb280a | ||
|
|
99010b6eae | ||
|
|
b2dabf06bf | ||
|
|
67ae05f473 | ||
|
|
fb4fecf411 | ||
|
|
c855f011d1 | ||
|
|
02717eb2fb | ||
|
|
de70ebe769 | ||
|
|
c6109fd396 | ||
|
|
83584a3175 | ||
|
|
f5dcc52b3b | ||
|
|
1901964de1 | ||
|
|
80e9c2452b | ||
|
|
5ad4b39160 | ||
|
|
89b73a9cfa | ||
|
|
e2d8334d69 | ||
|
|
9b16d7acc0 | ||
|
|
6836840746 | ||
|
|
4084d301ca | ||
|
|
added21b18 | ||
|
|
8cd77391cc | ||
|
|
05ebfccc63 | ||
|
|
cb3a690294 | ||
|
|
194a7b0e57 | ||
|
|
98e4d01feb | ||
|
|
c22e3895b5 | ||
|
|
9a76c19615 | ||
|
|
59fa088975 | ||
|
|
163244f40f | ||
|
|
a89b53af4f | ||
|
|
35508e253d | ||
|
|
e586fec338 | ||
|
|
93fa27bdba | ||
|
|
048856c333 | ||
|
|
986f1162dd | ||
|
|
dc8dfa9f0c | ||
|
|
82e7094f3a | ||
|
|
f0e62004d5 | ||
|
|
bbdbafd8db | ||
|
|
6121efec59 | ||
|
|
4fdbcc25a0 | ||
|
|
ca42425b33 | ||
|
|
ce0f61b66d | ||
|
|
13ee71f351 | ||
|
|
c57494d7cd | ||
|
|
d2e74ab330 | ||
|
|
850dde1a06 | ||
|
|
5e83f301ff | ||
|
|
5e74dd7a6d | ||
|
|
8a273e01e9 | ||
|
|
75f6f8dd18 | ||
|
|
e1cf0c4ea7 | ||
|
|
cc134abd12 | ||
|
|
b7db676cba | ||
|
|
3881996560 | ||
|
|
527d28ad81 | ||
|
|
f2371e8a80 | ||
|
|
fd882834d3 | ||
|
|
f92a720d63 | ||
|
|
d6f58698b7 | ||
|
|
d090260b17 | ||
|
|
cd43c1c01f | ||
|
|
284b28e8d9 | ||
|
|
b50547d868 | ||
|
|
401046fbe5 | ||
|
|
6e82509964 | ||
|
|
ab6fa490e5 | ||
|
|
55e2780f50 | ||
|
|
f4803c675c | ||
|
|
90514c603f | ||
|
|
7f4137e7cc | ||
|
|
071cb9af2b | ||
|
|
6ce1550457 | ||
|
|
8cb5d44dc9 | ||
|
|
1331b3f87c | ||
|
|
ab96297e58 | ||
|
|
c4fd3a74c5 | ||
|
|
da922fb2a7 | ||
|
|
4a05c4be40 | ||
|
|
cef30c8e2d | ||
|
|
8417498f08 | ||
|
|
10e941cea6 | ||
|
|
3d7b1ca799 | ||
|
|
b236354fc7 | ||
|
|
6fbb5a380d | ||
|
|
054eb42613 | ||
|
|
6074f4475d | ||
|
|
7afd3fd6a2 | ||
|
|
7d415e40b2 | ||
|
|
3c89d252d2 | ||
|
|
f678873e9f | ||
|
|
17d37a062a | ||
|
|
14702063f2 | ||
|
|
c599d8a0ed | ||
|
|
207d5adceb | ||
|
|
b4c46ce222 | ||
|
|
6fe269193a | ||
|
|
d948543d5c | ||
|
|
a327e6c0a7 | ||
|
|
fbd35b7974 | ||
|
|
b94a22e6a7 | ||
|
|
63ea9e4a21 | ||
|
|
e028d8ea31 | ||
|
|
457a7a14e5 | ||
|
|
cd387328be | ||
|
|
5524476787 | ||
|
|
78526fb405 | ||
|
|
b2dee43bb0 | ||
|
|
60e9443b12 | ||
|
|
ab8fa52ca4 | ||
|
|
16f64f6247 | ||
|
|
98992c656f | ||
|
|
053e026982 | ||
|
|
74180a4381 | ||
|
|
293725f933 | ||
|
|
c33f8d0ea2 | ||
|
|
0f2cd3cb7f | ||
|
|
2441ac5e77 | ||
|
|
f248001460 | ||
|
|
1fe2c93946 | ||
|
|
a3d50605c1 | ||
|
|
5427152f15 | ||
|
|
a4e9c2fdde | ||
|
|
e244b2dc51 | ||
|
|
31dea8fa99 | ||
|
|
be8cf56240 | ||
|
|
0bc7412430 | ||
|
|
6d56e92306 | ||
|
|
97c94f8fcc | ||
|
|
92b5a5296d | ||
|
|
febca6d441 | ||
|
|
bf29abc468 | ||
|
|
97cb30565c | ||
|
|
9809f050ef | ||
|
|
7ecfb24584 | ||
|
|
4aa65f750e | ||
|
|
63c533fa62 | ||
|
|
8751d6c315 | ||
|
|
eb40dc2d5d | ||
|
|
c9d1a955b9 | ||
|
|
c346fd0602 | ||
|
|
a45550b0c6 | ||
|
|
a843c59d77 | ||
|
|
3bfb29a51c | ||
|
|
d8ede3263f | ||
|
|
c36e43e249 | ||
|
|
52150b3067 | ||
|
|
e7796cc917 | ||
|
|
7f560e6a65 | ||
|
|
8f233221e5 | ||
|
|
f700abd65d | ||
|
|
f9e4d6d617 | ||
|
|
d9286765e1 | ||
|
|
a7cde9364a | ||
|
|
070d74830b | ||
|
|
23c07c4e82 | ||
|
|
ab7824aa85 | ||
|
|
67bafbe44e | ||
|
|
c4805b8152 | ||
|
|
33729289c7 | ||
|
|
721e3d9ef9 | ||
|
|
a72e22e046 | ||
|
|
36749658da | ||
|
|
4ad3dc0bc7 | ||
|
|
73864b923d | ||
|
|
71277e979a | ||
|
|
60fef3de74 | ||
|
|
0fe503fa0e | ||
|
|
db02d9f1ba | ||
|
|
89a987d03a | ||
|
|
3284cea6f2 | ||
|
|
df5ad554c1 | ||
|
|
07ae92943d | ||
|
|
175712d4a9 | ||
|
|
3f1b253c31 | ||
|
|
202d0b86b3 | ||
|
|
4e87affb0b | ||
|
|
7f83d37b3c | ||
|
|
09e81b00c5 | ||
|
|
68b77e22c5 | ||
|
|
c5ddd8302a | ||
|
|
1a0fe0429a | ||
|
|
6646232311 | ||
|
|
c1e88c3e83 | ||
|
|
c91a534d1a | ||
|
|
752100f1ce | ||
|
|
dae083c540 | ||
|
|
857c3a4f8d | ||
|
|
acc4c4756e | ||
|
|
0bacb215c5 | ||
|
|
d1a6e60b01 | ||
|
|
8f954151ca | ||
|
|
145d4eaaed | ||
|
|
7673232793 | ||
|
|
f08951a9b9 | ||
|
|
ff963a9df4 | ||
|
|
f30c74b987 | ||
|
|
467dbdd183 | ||
|
|
0e78deb8f2 | ||
|
|
50cc7ae632 | ||
|
|
512d765d60 | ||
|
|
7b9331bd14 | ||
|
|
4f5ce9fb43 | ||
|
|
83f73eb631 | ||
|
|
3f1b89d665 | ||
|
|
8f6fc97fc8 | ||
|
|
a463e6f9d7 | ||
|
|
2a0364ca8f | ||
|
|
23e9ed93bb | ||
|
|
689343d1c9 | ||
|
|
d01dc334d7 | ||
|
|
58a3791a5c | ||
|
|
1a133af141 | ||
|
|
938fb6509e | ||
|
|
d3bfd99210 | ||
|
|
3a1740fdca | ||
|
|
e3de403c6c | ||
|
|
5c1ab02753 | ||
|
|
e5713df008 | ||
|
|
95ea64980b | ||
|
|
c0a77b790d | ||
|
|
e073b4331a | ||
|
|
e919901487 | ||
|
|
13f9f106be | ||
|
|
e9c7053ef5 | ||
|
|
62051c8809 | ||
|
|
0a6a794e23 | ||
|
|
01846657d1 | ||
|
|
dd7545afdf | ||
|
|
9296013596 | ||
|
|
8015c6cc88 | ||
|
|
8c93444286 | ||
|
|
96ed7cae0d | ||
|
|
72c4628b79 | ||
|
|
1117631717 | ||
|
|
60176060cb | ||
|
|
d949b54ef4 | ||
|
|
720db64a03 | ||
|
|
37e0978bfc | ||
|
|
cf355b0b69 | ||
|
|
f2edb54b8b | ||
|
|
13ec27039d | ||
|
|
ad3805a560 | ||
|
|
cc4afa7b62 | ||
|
|
7a6bfed445 | ||
|
|
f2a7b687a9 | ||
|
|
522d021264 | ||
|
|
14a0db1f34 | ||
|
|
430883987a | ||
|
|
f17b641d46 | ||
|
|
48a8290e92 | ||
|
|
46acf4de1c | ||
|
|
17792a1603 | ||
|
|
a8611f5d80 | ||
|
|
deef938e97 | ||
|
|
e014cfa72a | ||
|
|
aa242eba25 | ||
|
|
0ac5569d60 | ||
|
|
7c3e3442c2 | ||
|
|
0f894dcdfe | ||
|
|
361d987f47 | ||
|
|
6017db260a | ||
|
|
f9c9b3d981 | ||
|
|
e7b876f4e6 | ||
|
|
554b948775 | ||
|
|
9bdbb59401 | ||
|
|
377861c5f1 | ||
|
|
26c2aaf567 | ||
|
|
62b54ceaaf | ||
|
|
625eba76c7 | ||
|
|
e12bda3f42 | ||
|
|
0f0301ae5c | ||
|
|
8d1776af98 | ||
|
|
c597738915 | ||
|
|
639329dbe4 | ||
|
|
92b0525b6e | ||
|
|
06306aabd5 | ||
|
|
225905fcdb | ||
|
|
8ca2b3c641 | ||
|
|
ac1737492b |
@@ -19,7 +19,6 @@ indocker
|
||||
docker-*
|
||||
phpstan.neon
|
||||
php*xml*
|
||||
infection*
|
||||
**/test*
|
||||
build*
|
||||
**/.*
|
||||
|
||||
4
.github/DISCUSSION_TEMPLATE/help-wanted.yml
vendored
4
.github/DISCUSSION_TEMPLATE/help-wanted.yml
vendored
@@ -20,10 +20,8 @@ body:
|
||||
options:
|
||||
- Self-hosted Apache
|
||||
- Self-hosted nginx
|
||||
- Self-hosted openswoole
|
||||
- Self-hosted RoadRunner
|
||||
- Openswoole Docker image
|
||||
- RoadRunner Docker image
|
||||
- Docker image
|
||||
- Other (explain in summary)
|
||||
- type: dropdown
|
||||
validations:
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
github: ['acelaya']
|
||||
custom: ['https://acel.me/donate']
|
||||
custom: ['https://slnk.to/donate']
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/Bug.yml
vendored
17
.github/ISSUE_TEMPLATE/Bug.yml
vendored
@@ -22,10 +22,8 @@ body:
|
||||
options:
|
||||
- Self-hosted Apache
|
||||
- Self-hosted nginx
|
||||
- Self-hosted openswoole
|
||||
- Self-hosted RoadRunner
|
||||
- Openswoole Docker image
|
||||
- RoadRunner Docker image
|
||||
- Docker image
|
||||
- Other (explain in summary)
|
||||
- type: dropdown
|
||||
validations:
|
||||
@@ -60,5 +58,14 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
value: '<!-- Provide steps to reproduce the bug. -->'
|
||||
label: Minimum steps to reproduce
|
||||
value: |
|
||||
<!--
|
||||
Simple but detailed way to reproduce the bug:
|
||||
|
||||
* Avoid things like "create a kubernetes cluster", or anything related with cloud providers, as that is rarely the root cause.
|
||||
* Avoid too vague steps or one-liners like "Update from v1 to v2".
|
||||
* Providing the reproduction in the form of a self-contained docker-composer is desirable.
|
||||
|
||||
Failing in any of these will cause the issue to be closed as "not reproducible".
|
||||
-->
|
||||
|
||||
7
.github/actions/ci-setup/action.yml
vendored
7
.github/actions/ci-setup/action.yml
vendored
@@ -12,7 +12,6 @@ inputs:
|
||||
php-extensions:
|
||||
description: 'The PHP extensions to install'
|
||||
required: false
|
||||
default: ''
|
||||
extensions-cache-key:
|
||||
description: 'The key used to cache PHP extensions. If empty value is provided, extension caching is disabled'
|
||||
required: true
|
||||
@@ -21,6 +20,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup cache environment
|
||||
if: ${{ inputs.php-extensions }}
|
||||
id: extcache
|
||||
uses: shivammathur/cache-extensions@v1
|
||||
with:
|
||||
@@ -28,7 +28,8 @@ runs:
|
||||
extensions: ${{ inputs.php-extensions }}
|
||||
key: ${{ inputs.extensions-cache-key }}
|
||||
- name: Cache extensions
|
||||
uses: actions/cache@v3
|
||||
if: ${{ inputs.php-extensions }}
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.extcache.outputs.dir }}
|
||||
key: ${{ steps.extcache.outputs.key }}
|
||||
@@ -43,5 +44,5 @@ runs:
|
||||
ini-values: pcov.directory=module
|
||||
- name: Install dependencies
|
||||
if: ${{ inputs.install-deps == 'yes' }}
|
||||
run: composer install --no-interaction --prefer-dist ${{ inputs.php-version == '8.3' && '--ignore-platform-reqs' || '' }}
|
||||
run: composer install --no-interaction --prefer-dist ${{ inputs.php-version == '8.4' && '--ignore-platform-req=php' || '' }}
|
||||
shell: bash
|
||||
|
||||
16
.github/workflows/ci-db-tests.yml
vendored
16
.github/workflows/ci-db-tests.yml
vendored
@@ -10,11 +10,11 @@ on:
|
||||
|
||||
jobs:
|
||||
db-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2', '8.3']
|
||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
||||
php-version: ['8.2', '8.3', '8.4']
|
||||
continue-on-error: ${{ matrix.php-version == '8.4' }}
|
||||
env:
|
||||
LC_ALL: C
|
||||
steps:
|
||||
@@ -24,20 +24,20 @@ jobs:
|
||||
run: sudo ./data/infra/ci/install-ms-odbc.sh
|
||||
- name: Start database server
|
||||
if: ${{ inputs.platform != 'sqlite:ci' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_${{ inputs.platform }}
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_${{ inputs.platform }}
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.1.0, pdo_sqlsrv-5.11.1
|
||||
php-extensions: pdo_sqlsrv-5.12.0
|
||||
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
|
||||
- name: Create test database
|
||||
if: ${{ inputs.platform == 'ms' }}
|
||||
run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;"
|
||||
run: docker compose exec -T shlink_db_ms /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;"
|
||||
- name: Run tests
|
||||
run: composer test:db:${{ inputs.platform }}
|
||||
- name: Upload code coverage
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ matrix.php-version == '8.2' && inputs.platform == 'sqlite:ci' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.php-version == '8.3' && inputs.platform == 'sqlite:ci' }}
|
||||
with:
|
||||
name: coverage-db
|
||||
path: |
|
||||
|
||||
2
.github/workflows/ci-docker-image-build.yml
vendored
2
.github/workflows/ci-docker-image-build.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build-docker-image:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
46
.github/workflows/ci-mutation-tests.yml
vendored
46
.github/workflows/ci-mutation-tests.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Mutation tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
test-group:
|
||||
type: string
|
||||
required: true
|
||||
description: One of unit, db, api or cli
|
||||
|
||||
jobs:
|
||||
mutation-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2', '8.3']
|
||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: coverage-${{ inputs.test-group }}
|
||||
path: build
|
||||
- name: Resolve infection args
|
||||
id: infection_args
|
||||
run: echo "args=--logger-github=false" >> $GITHUB_OUTPUT
|
||||
# TODO Try to filter mutation tests to improve execution times. Investigate why --git-diff-lines --git-diff-base=develop does not work
|
||||
# run: |
|
||||
# BRANCH="${GITHUB_REF#refs/heads/}" |
|
||||
# if [[ $BRANCH == 'main' || $BRANCH == 'develop' ]]; then
|
||||
# echo "args=--logger-github=false" >> $GITHUB_OUTPUT
|
||||
# else
|
||||
# echo "args=--logger-github=false --git-diff-lines --git-diff-base=develop" >> $GITHUB_OUTPUT
|
||||
# fi;
|
||||
shell: bash
|
||||
- if: ${{ inputs.test-group == 'unit' }}
|
||||
run: composer infect:ci:unit -- ${{ steps.infection_args.outputs.args }}
|
||||
env:
|
||||
INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }}
|
||||
- if: ${{ inputs.test-group != 'unit' }}
|
||||
run: composer infect:ci:${{ inputs.test-group }} -- ${{ steps.infection_args.outputs.args }}
|
||||
20
.github/workflows/ci-tests.yml
vendored
20
.github/workflows/ci-tests.yml
vendored
@@ -10,27 +10,31 @@ on:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2', '8.3']
|
||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
||||
php-version: ['8.2', '8.3', '8.4']
|
||||
continue-on-error: ${{ matrix.php-version == '8.4' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # rr get-binary picks this env automatically
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Start postgres database server
|
||||
if: ${{ inputs.test-group == 'api' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
- name: Start maria database server
|
||||
if: ${{ inputs.test-group == 'cli' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_maria
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_maria
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
||||
- name: Download RoadRunner binary
|
||||
if: ${{ inputs.test-group == 'api' }}
|
||||
run: ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr
|
||||
- run: composer test:${{ inputs.test-group }}:ci
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ matrix.php-version == '8.2' }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.php-version == '8.3' }}
|
||||
with:
|
||||
name: coverage-${{ inputs.test-group }}
|
||||
path: |
|
||||
|
||||
105
.github/workflows/ci.yml
vendored
105
.github/workflows/ci.yml
vendored
@@ -8,7 +8,6 @@ on:
|
||||
- '*.md'
|
||||
- '*.xml'
|
||||
- '*.yml*'
|
||||
- '*.json5'
|
||||
- '*.neon'
|
||||
push:
|
||||
branches:
|
||||
@@ -21,22 +20,20 @@ on:
|
||||
- '*.md'
|
||||
- '*.xml'
|
||||
- '*.yml*'
|
||||
- '*.json5'
|
||||
- '*.neon'
|
||||
|
||||
jobs:
|
||||
static-analysis:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2']
|
||||
php-version: ['8.3']
|
||||
command: ['cs', 'stan', 'swagger:validate']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
|
||||
- run: composer ${{ matrix.command }}
|
||||
|
||||
@@ -50,124 +47,54 @@ jobs:
|
||||
with:
|
||||
test-group: cli
|
||||
|
||||
openswoole-api-tests:
|
||||
api-tests:
|
||||
uses: './.github/workflows/ci-tests.yml'
|
||||
with:
|
||||
test-group: api
|
||||
|
||||
roadrunner-api-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
db-tests:
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2', '8.3']
|
||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # rr get-binary picks this env automatically
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
- run: composer install --no-interaction --prefer-dist --ignore-platform-req=ext-openswoole ${{ matrix.php-version == '8.3' && '--ignore-platform-reqs' || '' }}
|
||||
- run: ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr
|
||||
- run: composer test:api:rr
|
||||
|
||||
sqlite-db-tests:
|
||||
platform: ['sqlite:ci', 'mysql', 'maria', 'postgres', 'ms']
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'sqlite:ci'
|
||||
|
||||
mysql-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'mysql'
|
||||
|
||||
maria-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'maria'
|
||||
|
||||
postgres-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'postgres'
|
||||
|
||||
ms-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'ms'
|
||||
|
||||
unit-mutation-tests:
|
||||
needs:
|
||||
- unit-tests
|
||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||
with:
|
||||
test-group: unit
|
||||
|
||||
db-mutation-tests:
|
||||
needs:
|
||||
- sqlite-db-tests
|
||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||
with:
|
||||
test-group: db
|
||||
|
||||
api-mutation-tests:
|
||||
needs:
|
||||
- openswoole-api-tests
|
||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||
with:
|
||||
test-group: api
|
||||
|
||||
cli-mutation-tests:
|
||||
needs:
|
||||
- cli-tests
|
||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||
with:
|
||||
test-group: cli
|
||||
platform: ${{ matrix.platform }}
|
||||
|
||||
upload-coverage:
|
||||
needs:
|
||||
- unit-tests
|
||||
- openswoole-api-tests
|
||||
- api-tests
|
||||
- cli-tests
|
||||
- sqlite-db-tests
|
||||
runs-on: ubuntu-22.04
|
||||
- db-tests
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2']
|
||||
php-version: ['8.3']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
coverage: pcov
|
||||
ini-values: pcov.directory=module
|
||||
- uses: actions/download-artifact@v3
|
||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: build
|
||||
- run: mv build/coverage-unit/coverage-unit.cov build/coverage-unit.cov
|
||||
- run: mv build/coverage-db/coverage-db.cov build/coverage-db.cov
|
||||
- run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov
|
||||
- run: mv build/coverage-cli/coverage-cli.cov build/coverage-cli.cov
|
||||
- run: wget https://phar.phpunit.de/phpcov-9.0.0.phar
|
||||
- run: php phpcov-9.0.0.phar merge build --clover build/clover.xml
|
||||
- run: vendor/bin/phpcov merge build --clover build/clover.xml
|
||||
- name: Publish coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./build/clover.xml
|
||||
|
||||
delete-artifacts:
|
||||
needs:
|
||||
- unit-mutation-tests
|
||||
- db-mutation-tests
|
||||
- api-mutation-tests
|
||||
- cli-mutation-tests
|
||||
- upload-coverage
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: geekyeggo/delete-artifact@v2
|
||||
with:
|
||||
|
||||
10
.github/workflows/publish-docker-image.yml
vendored
10
.github/workflows/publish-docker-image.yml
vendored
@@ -15,14 +15,7 @@ jobs:
|
||||
- runtime: 'rr'
|
||||
tag-suffix: 'roadrunner'
|
||||
platforms: 'linux/arm64/v8,linux/amd64'
|
||||
- runtime: 'openswoole'
|
||||
tag-suffix: 'openswoole'
|
||||
platforms: 'linux/arm/v7,linux/arm64/v8,linux/amd64'
|
||||
- runtime: 'rr'
|
||||
tag-suffix: 'non-root'
|
||||
platforms: 'linux/arm64/v8,linux/amd64'
|
||||
user-id: '1001'
|
||||
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
|
||||
uses: shlinkio/github-actions/.github/workflows/docker-publish-image.yml@main
|
||||
secrets: inherit
|
||||
with:
|
||||
image-name: shlinkio/shlink
|
||||
@@ -31,4 +24,3 @@ jobs:
|
||||
tags-suffix: ${{ matrix.tag-suffix }}
|
||||
extra-build-args: |
|
||||
SHLINK_RUNTIME=${{ matrix.runtime }}
|
||||
SHLINK_USER_ID=${{ matrix.user-id && matrix.user-id || 'root' }}
|
||||
|
||||
21
.github/workflows/publish-release.yml
vendored
21
.github/workflows/publish-release.yml
vendored
@@ -7,34 +7,29 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2', '8.3']
|
||||
swoole: ['yes', 'no']
|
||||
php-version: ['8.2', '8.3'] # TODO 8.4
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||
install-deps: 'no'
|
||||
- if: ${{ matrix.swoole == 'yes' }}
|
||||
run: ./build.sh ${GITHUB_REF#refs/tags/v}
|
||||
- if: ${{ matrix.swoole == 'no' }}
|
||||
run: ./build.sh ${GITHUB_REF#refs/tags/v} --no-swoole
|
||||
- uses: actions/upload-artifact@v3
|
||||
- run: ./build.sh ${GITHUB_REF#refs/tags/v}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-files-${{ matrix.php-version }}-${{ matrix.swoole }}
|
||||
name: dist-files-${{ matrix.php-version }}
|
||||
path: build
|
||||
|
||||
publish:
|
||||
needs: ['build']
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: build
|
||||
- name: Publish release with assets
|
||||
@@ -48,7 +43,7 @@ jobs:
|
||||
|
||||
delete-artifacts:
|
||||
needs: ['publish']
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: geekyeggo/delete-artifact@v2
|
||||
with:
|
||||
|
||||
3
.github/workflows/publish-swagger-spec.yml
vendored
3
.github/workflows/publish-swagger-spec.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2']
|
||||
@@ -20,7 +20,6 @@ jobs:
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||
- run: composer swagger:inline
|
||||
- run: mkdir ${{ steps.determine_version.outputs.version }}
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
.idea
|
||||
bin/rr
|
||||
config/roadrunner/.pid
|
||||
.pid
|
||||
build
|
||||
!docker/build
|
||||
composer.lock
|
||||
@@ -15,3 +15,4 @@ docs/mercure.html
|
||||
docker-compose.override.yml
|
||||
.phpunit.result.cache
|
||||
docs/swagger/swagger-inlined.json
|
||||
phpcov*
|
||||
|
||||
2311
CHANGELOG.md
2311
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,7 @@ Then you will have to follow these steps:
|
||||
* Run `./indocker bin/cli db:migrate` to get database migrations up to date.
|
||||
* Run `./indocker bin/cli api-key:generate` to get your first API key generated.
|
||||
|
||||
Once you finish this, you will have the project exposed in ports `8800` through RoadRunner, `8080` through openswoole and `8000` through nginx+php-fpm.
|
||||
Once you finish this, you will have the project exposed in ports `8800` through RoadRunner and `8000` through nginx+php-fpm.
|
||||
|
||||
> Note: The `indocker` shell script is a helper tool used to run commands inside the main docker container.
|
||||
|
||||
@@ -46,17 +46,18 @@ This is a simplified version of the project structure:
|
||||
```
|
||||
shlink
|
||||
├── bin
|
||||
│ └── cli
|
||||
│ ├── cli
|
||||
│ └── [...]
|
||||
├── config
|
||||
│ ├── autoload
|
||||
│ ├── params
|
||||
│ ├── config.php
|
||||
│ └── container.php
|
||||
│ ├── container.php
|
||||
│ └── [...]
|
||||
├── data
|
||||
│ ├── cache
|
||||
│ ├── locks
|
||||
│ ├── log
|
||||
│ ├── migrations
|
||||
│ └── proxies
|
||||
├── docs
|
||||
│ ├── adr
|
||||
@@ -67,6 +68,7 @@ shlink
|
||||
│ ├── Core
|
||||
│ └── Rest
|
||||
├── public
|
||||
│ └── [...]
|
||||
├── composer.json
|
||||
└── README.md
|
||||
```
|
||||
@@ -75,10 +77,10 @@ The purposes of every folder are:
|
||||
|
||||
* `bin`: It contains the CLI tools. The `cli` one is the main entry point to run Shlink from the command line.
|
||||
* `config`: Contains application-wide configurations, which are later merged with the ones provided by every module.
|
||||
* `data`: Common runtime-generated git-ignored assets, like logs, caches, etc.
|
||||
* `data`: Common git-ignored assets, like logs, caches, lock files, GeoLite DB files, etc. It's the only location where Shlink may need to write at runtime.
|
||||
* `docs`: Any project documentation is stored here, like API spec definitions or architectural decision records.
|
||||
* `module`: Contains a sub-folder for every module in the project. Modules contain the source code, tests and configurations for every context in the project.
|
||||
* `public`: Few assets (like `favicon.ico` or `robots.txt`) and the web entry point are stored here. This web entry point is not used when serving the app with RoadRunner or openswoole.
|
||||
* `public`: Few assets (like `favicon.ico` or `robots.txt`) and the web entry point are stored here. This web entry point is not used when serving the app with RoadRunner.
|
||||
|
||||
## Project tests
|
||||
|
||||
@@ -94,7 +96,7 @@ In order to ensure stability and no regressions are introduced while developing
|
||||
|
||||
The project provides some tooling to run them against any of the supported database engines.
|
||||
|
||||
* **API tests**: These are E2E tests that spin up an instance of the app with RoadRunner or openswoole, and test it from the outside by interacting with the REST API.
|
||||
* **API tests**: These are E2E tests that spin up an instance of the app with RoadRunner, and test it from the outside by interacting with the REST API.
|
||||
|
||||
These are the best tests to catch regressions, and to verify everything behaves as expected.
|
||||
|
||||
@@ -122,7 +124,6 @@ Depending on the kind of contribution, maybe not all kinds of tests are needed,
|
||||
|
||||
* Run `./indocker composer test:api` to run API E2E tests. For these, the Postgres database engine is used.
|
||||
* Run `./indocker composer test:cli` to run CLI E2E tests. For these, the Maria DB database engine is used.
|
||||
* Run `./indocker composer infect:test` to run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/).
|
||||
* Run `./indocker composer ci` to run all previous commands together, parallelizing non-conflicting tasks as much as possible.
|
||||
|
||||
## Testing endpoints
|
||||
|
||||
28
Dockerfile
28
Dockerfile
@@ -1,14 +1,12 @@
|
||||
FROM php:8.2-alpine3.17 as base
|
||||
FROM php:8.3-alpine3.19 as base
|
||||
|
||||
ARG SHLINK_VERSION=latest
|
||||
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
||||
ARG SHLINK_RUNTIME=rr
|
||||
ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
|
||||
ARG SHLINK_USER_ID='root'
|
||||
ENV SHLINK_USER_ID ${SHLINK_USER_ID}
|
||||
|
||||
ENV OPENSWOOLE_VERSION 22.1.0
|
||||
ENV PDO_SQLSRV_VERSION 5.11.1
|
||||
ENV USER_ID '1001'
|
||||
ENV PDO_SQLSRV_VERSION 5.12.0
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
ENV LC_ALL 'C'
|
||||
@@ -26,13 +24,8 @@ RUN \
|
||||
apk del .dev-deps && \
|
||||
apk add --no-cache postgresql icu libzip libpng
|
||||
|
||||
# Install openswoole and sqlsrv driver for x86_64 builds
|
||||
# Install sqlsrv driver for x86_64 builds
|
||||
RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
||||
# Openswoole is deprecated. Remove in v4.0.0
|
||||
pecl install openswoole-${OPENSWOOLE_VERSION} && \
|
||||
docker-php-ext-enable openswoole ; \
|
||||
fi; \
|
||||
if [ $(uname -m) == "x86_64" ]; then \
|
||||
wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
@@ -47,14 +40,7 @@ FROM base as builder
|
||||
COPY . .
|
||||
COPY --from=composer:2 /usr/bin/composer ./composer.phar
|
||||
RUN apk add --no-cache git && \
|
||||
# FIXME Ignoring ext-openswoole platform req, as it makes install fail with roadrunner, even though it's a dev dependency and we are passing --no-dev
|
||||
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction --ignore-platform-req=ext-openswoole && \
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
||||
# Openswoole is deprecated. Remove in v4.0.0
|
||||
php composer.phar remove spiral/roadrunner spiral/roadrunner-jobs spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction ; \
|
||||
elif [ "$SHLINK_RUNTIME" == 'rr' ]; then \
|
||||
php composer.phar remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction --ignore-platform-req=ext-openswoole ; \
|
||||
fi; \
|
||||
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction && \
|
||||
php composer.phar clear-cache && \
|
||||
rm -r docker composer.* && \
|
||||
sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php
|
||||
@@ -64,7 +50,7 @@ RUN apk add --no-cache git && \
|
||||
FROM base
|
||||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||
|
||||
COPY --from=builder --chown=${SHLINK_USER_ID} /etc/shlink .
|
||||
COPY --from=builder --chown=${USER_ID} /etc/shlink .
|
||||
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink && \
|
||||
if [ "$SHLINK_RUNTIME" == 'rr' ]; then \
|
||||
php ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr ; \
|
||||
@@ -78,6 +64,6 @@ COPY docker/docker-entrypoint.sh docker-entrypoint.sh
|
||||
COPY docker/config/shlink_in_docker.local.php config/autoload/shlink_in_docker.local.php
|
||||
COPY docker/config/php.ini ${PHP_INI_DIR}/conf.d/
|
||||
|
||||
USER ${SHLINK_USER_ID}
|
||||
USER ${USER_ID}
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "./docker-entrypoint.sh"]
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2023 Alejandro Celaya
|
||||
Copyright (c) 2016-2024 Alejandro Celaya
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
10
README.md
10
README.md
@@ -2,12 +2,13 @@
|
||||
|
||||
[](https://github.com/shlinkio/shlink/actions/workflows/ci.yml?query=workflow%3A%22Continuous+integration%22)
|
||||
[](https://app.codecov.io/gh/shlinkio/shlink)
|
||||
[](https://dashboard.stryker-mutator.io/reports/github.com/shlinkio/shlink/develop)
|
||||
[](https://packagist.org/packages/shlinkio/shlink)
|
||||
[](https://hub.docker.com/r/shlinkio/shlink/)
|
||||
[](https://github.com/shlinkio/shlink/blob/main/LICENSE)
|
||||
[](https://twitter.com/shlinkio)
|
||||
|
||||
[](https://fosstodon.org/@shlinkio)
|
||||
[](https://bsky.app/profile/shlinkio.bsky.social)
|
||||
[](https://twitter.com/shlinkio)
|
||||
[](https://slnk.to/donate)
|
||||
|
||||
A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own domain.
|
||||
@@ -38,12 +39,11 @@ First, make sure the host where you are going to run shlink fulfills these requi
|
||||
|
||||
* PHP 8.2 or 8.3
|
||||
* The next PHP extensions: json, curl, pdo, intl, gd and gmp/bcmath.
|
||||
* apcu extension is recommended if you don't plan to use openswoole.
|
||||
* apcu extension is recommended if you don't plan to use RoadRunner.
|
||||
* xml extension is required if you want to generate QR codes in svg format.
|
||||
* sockets and bcmath extensions are required if you want to integrate with a RabbitMQ instance.
|
||||
* MySQL, MariaDB, PostgreSQL, MicrosoftSQL or SQLite.
|
||||
* You will also need the corresponding pdo variation for the database you are planning to use: `pdo_mysql`, `pdo_pgsql`, `pdo_sqlsrv` or `pdo_sqlite`.
|
||||
* The [openswoole](https://openswoole.com/) PHP extension (if you plan to serve Shlink with openswoole) or the web server of your choice with PHP integration (like Apache or Nginx).
|
||||
|
||||
### Download
|
||||
|
||||
@@ -53,7 +53,7 @@ In order to run Shlink, you will need a built version of the project. There are
|
||||
|
||||
The easiest way to install shlink is by using one of the pre-bundled distributable packages.
|
||||
|
||||
Go to the [latest version](https://github.com/shlinkio/shlink/releases/latest) and download the `shlink*_dist.zip` file that suits your needs. You will find one for every supported PHP version and with/without openswoole integration.
|
||||
Go to the [latest version](https://github.com/shlinkio/shlink/releases/latest) and download the `shlink*_dist.zip` file that suits your needs. You will find one for every supported PHP version.
|
||||
|
||||
Finally, decompress the file in the location of your choice.
|
||||
|
||||
|
||||
50
UPGRADE.md
50
UPGRADE.md
@@ -1,5 +1,55 @@
|
||||
# Upgrading
|
||||
|
||||
## From v3.x to v4.x
|
||||
|
||||
### General
|
||||
|
||||
* Swoole and Openswoole are no longer officially supported runtimes. The recommended alternative is RoadRunner.
|
||||
* Dist files for swoole/openswoole are no longer published.
|
||||
* Webhooks are no longer supported. Migrate to one of the other [real-time updates](https://shlink.io/documentation/advanced/real-time-updates/) mechanisms.
|
||||
* When using RoadRunner, the amount of web workers, task workers and the port number can no longer be provided via config options. Use `WEB_WORKER_NUM`, `TASK_WORKER_NUM` and `PORT` env vars instead.
|
||||
|
||||
### Changes in URL shortener
|
||||
|
||||
* The short URLs `loosely` mode is no longer supported, as it was a typo. Use `loose` mode instead.
|
||||
* QR codes URLs now work by default, even for short URLs that cannot be visited due to max visits or date range limitations.
|
||||
If you want to keep previous behavior, pass `QR_CODE_FOR_DISABLED_SHORT_URLS=false` or the equivalent configuration option.
|
||||
* Long URL title resolution is now enabled by default. You can still disable it by passing `AUTO_RESOLVE_TITLES=false` or the equivalent configuration option.
|
||||
* Shlink no longer allows to opt-in for long URL verification. Long URLs are unconditionally considered correct during short URL creation/edition.
|
||||
* Device long URLs have been migrated to the new Dynamic rule-based redirects system and will continue to work as expected, but the API surface has changed.
|
||||
If you use shlink-web-client and rely on this feature when creating/updating short URLs, **DO NOT UPDATE YET**. Support for dynamic rule-based redirects will be added to shlink-web-client soon, in v4.1.0
|
||||
|
||||
### Changes in REST API
|
||||
|
||||
* REST API v1/v2 now behave like v3. This only affects error codes, which are now proper URIs.
|
||||
* `INVALID_ARGUMENT` -> `https://shlink.io/api/error/invalid-data`
|
||||
* `INVALID_SHORT_URL_DELETION` -> `https://shlink.io/api/error/invalid-short-url-deletion`
|
||||
* `DOMAIN_NOT_FOUND` -> `https://shlink.io/api/error/domain-not-found`
|
||||
* `FORBIDDEN_OPERATION` -> `https://shlink.io/api/error/forbidden-tag-operation`
|
||||
* `INVALID_SLUG` -> `https://shlink.io/api/error/non-unique-slug`
|
||||
* `INVALID_SHORTCODE` -> `https://shlink.io/api/error/short-url-not-found`
|
||||
* `TAG_CONFLICT` -> `https://shlink.io/api/error/tag-conflict`
|
||||
* `TAG_NOT_FOUND` -> `https://shlink.io/api/error/tag-not-found`
|
||||
* `MERCURE_NOT_CONFIGURED` -> `https://shlink.io/api/error/mercure-not-configured`
|
||||
* `INVALID_AUTHORIZATION` -> `https://shlink.io/api/error/missing-authentication`
|
||||
* `INVALID_API_KEY` -> `https://shlink.io/api/error/invalid-api-key`
|
||||
* Endpoints previously returning props like `"visitsCount": {number}` no longer do it. There should be an alternative `"visitsSummary": {}` object with the amount nested on it.
|
||||
* It is no longer possible to order the short URLs list with `orderBy=visitsCount-ASC`/`orderBy=visitsCount-DESC`. Use `orderBy=visits-ASC`/`orderBy=visits-DESC` instead.
|
||||
* It is no longer possible to get tags with stats using `GET /tags?withStats=true`. Use `GET /tags/stats` endpoint instead.
|
||||
* The `deviceLongUrls` are ignored when calling `POST /short-urls` or `PATCH /short-urls/{shortCode}`. These should now be configured as dynamic rule-based redirects via `POST /short-urls/{shortCode}/redirect-rules`.
|
||||
|
||||
### Changes in Docker image
|
||||
|
||||
* Since openswoole is no longer supported, there are no longer image tags suffixed with `openswoole`. You should migrate to the default or `roadrunner` ones.
|
||||
* The `non-root` docker tag is no longer published, as all docker images are now running without super-user permissions.
|
||||
* Due to previous point, it is no longer possible to pass `ENABLE_PERIODIC_VISIT_LOCATE=true` in order to configure a cron job that locates visits periodically.
|
||||
This was not really needed in the docker image, as visits are located on the fly.
|
||||
|
||||
### Changes in integrations
|
||||
|
||||
* Credentials in redis URLs should now be URL-encoded, as they are unconditionally url-decoded before being used. Previously, it was possible to customize this behavior via `REDIS_DECODE_CREDENTIALS=true|false`.
|
||||
* Providing redis URIs in the form of `tcp://password@6.6.6.6:6379` is no longer supported. If you want to provide password with no username, do `tcp://:password@6.6.6.6:6379` instead.
|
||||
|
||||
## From v2.x to v3.x
|
||||
|
||||
### Changes in REST API
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
use Mezzio\Application;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware;
|
||||
use Shlinkio\Shlink\EventDispatcher\RoadRunner\RoadRunnerTaskConsumerToListener;
|
||||
use Spiral\RoadRunner\Http\PSR7Worker;
|
||||
|
||||
@@ -27,6 +28,9 @@ use function Shlinkio\Shlink\Config\env;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$container->get(RoadRunnerTaskConsumerToListener::class)->listenForTasks();
|
||||
$requestIdMiddleware = $container->get(RequestIdMiddleware::class);
|
||||
$container->get(RoadRunnerTaskConsumerToListener::class)->listenForTasks(
|
||||
fn (string $requestId) => $requestIdMiddleware->setCurrentRequestId($requestId),
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
export APP_ENV=test
|
||||
export TEST_ENV=api
|
||||
export TEST_RUNTIME="${TEST_RUNTIME:-"openswoole"}" # Openswoole is deprecated. Remove in v4.0.0
|
||||
export TEST_RUNTIME="${TEST_RUNTIME:-"rr"}" # rr is the only runtime currently supported
|
||||
export DB_DRIVER="${DB_DRIVER:-"postgres"}"
|
||||
export GENERATE_COVERAGE="${GENERATE_COVERAGE:-"no"}"
|
||||
|
||||
@@ -13,26 +13,19 @@ mkdir data/log/api-tests
|
||||
touch $OUTPUT_LOGS
|
||||
|
||||
# Try to stop server just in case it hanged in last execution
|
||||
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop
|
||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -f
|
||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -f -w .
|
||||
|
||||
echo 'Starting server...'
|
||||
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:start -d
|
||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr serve -p -c=config/roadrunner/.rr.dev.yml \
|
||||
-o=http.address=0.0.0.0:9999 \
|
||||
-o=logs.encoding=json \
|
||||
-o=logs.channels.http.encoding=json \
|
||||
-o=logs.channels.server.encoding=json \
|
||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr serve -p -w . -c=config/roadrunner/.rr.test.yml \
|
||||
-o=logs.output="${PWD}/${OUTPUT_LOGS}" \
|
||||
-o=logs.channels.http.output="${PWD}/${OUTPUT_LOGS}" \
|
||||
-o=logs.channels.server.output="${PWD}/${OUTPUT_LOGS}" &
|
||||
sleep 2 # Let's give the server a couple of seconds to start
|
||||
|
||||
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $*
|
||||
testsExitCode=$?
|
||||
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --testdox-summary $*
|
||||
TESTS_EXIT_CODE=$?
|
||||
|
||||
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop
|
||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -c config/roadrunner/.rr.dev.yml -o=http.address=0.0.0.0:9999
|
||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -w .
|
||||
|
||||
# Exit this script with the same code as the tests. If tests failed, this script has to fail
|
||||
exit $testsExitCode
|
||||
exit $TESTS_EXIT_CODE
|
||||
|
||||
22
build.sh
22
build.sh
@@ -1,18 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ] || ([ "$#" == 2 ] && [ "$2" != "--no-swoole" ]); then
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "Usage:" >&2
|
||||
echo " $0 {version} [--no-swoole]" >&2
|
||||
echo " $0 {version}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version=$1
|
||||
noSwoole=$2
|
||||
phpVersion=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')
|
||||
# Openswoole is deprecated. Remove in v4.0.0
|
||||
[[ $noSwoole ]] && swooleSuffix="" || swooleSuffix="_openswoole"
|
||||
distId="shlink${version}_php${phpVersion}${swooleSuffix}_dist"
|
||||
distId="shlink${version}_php${phpVersion}_dist"
|
||||
builtContent="./build/${distId}"
|
||||
projectdir=$(pwd)
|
||||
[[ -f ./composer.phar ]] && composerBin='./composer.phar' || composerBin='composer'
|
||||
@@ -31,19 +28,8 @@ cd "${builtContent}"
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies with $composerBin..."
|
||||
# Deprecated. Do not ignore PHP platform req for Shlink v4.0.0
|
||||
composerFlags="--optimize-autoloader --no-progress --no-interaction --ignore-platform-req=php+"
|
||||
${composerBin} self-update
|
||||
${composerBin} install --no-dev --prefer-dist $composerFlags
|
||||
|
||||
if [[ $noSwoole ]]; then
|
||||
# If generating a dist not for openswoole, uninstall mezzio-swoole
|
||||
${composerBin} remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev $composerFlags
|
||||
else
|
||||
# Deprecated. Remove in Shlink v4.0.0
|
||||
# If generating a dist for openswoole, uninstall RoadRunner
|
||||
${composerBin} remove spiral/roadrunner spiral/roadrunner-jobs spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies --update-no-dev $composerFlags
|
||||
fi
|
||||
${composerBin} install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction
|
||||
|
||||
# Delete development files
|
||||
echo 'Deleting dev files...'
|
||||
|
||||
136
composer.json
136
composer.json
@@ -16,67 +16,65 @@
|
||||
"ext-curl": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"akrabat/ip-address-middleware": "^2.1",
|
||||
"cakephp/chronos": "^3.0.2",
|
||||
"doctrine/dbal": "^4.1",
|
||||
"doctrine/migrations": "^3.6",
|
||||
"doctrine/orm": "^2.16",
|
||||
"endroid/qr-code": "^4.8",
|
||||
"doctrine/orm": "^3.2",
|
||||
"endroid/qr-code": "^5.0",
|
||||
"friendsofphp/proxy-manager-lts": "^1.0",
|
||||
"geoip2/geoip2": "^3.0",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"happyr/doctrine-specification": "^2.0",
|
||||
"hidehalo/nanoid-php": "^1.1",
|
||||
"jaybizzle/crawler-detect": "^1.2.116",
|
||||
"laminas/laminas-config": "^3.8",
|
||||
"laminas/laminas-config-aggregator": "^1.13",
|
||||
"laminas/laminas-config-aggregator": "^1.15",
|
||||
"laminas/laminas-diactoros": "^3.3",
|
||||
"laminas/laminas-inputfilter": "^2.27",
|
||||
"laminas/laminas-servicemanager": "^3.21",
|
||||
"laminas/laminas-stdlib": "^3.17",
|
||||
"league/uri": "^6.8",
|
||||
"matomo/matomo-php-tracker": "^3.2",
|
||||
"mezzio/mezzio": "^3.17",
|
||||
"mezzio/mezzio-fastroute": "^3.10",
|
||||
"mezzio/mezzio-fastroute": "^3.11",
|
||||
"mezzio/mezzio-problem-details": "^1.13",
|
||||
"mezzio/mezzio-swoole": "^4.7",
|
||||
"mlocati/ip-lib": "^1.18",
|
||||
"mobiledetect/mobiledetectlib": "^4.8",
|
||||
"pagerfanta/core": "^3.8",
|
||||
"php-middleware/request-id": "^4.1",
|
||||
"pugx/shortid-php": "^1.1",
|
||||
"ramsey/uuid": "^4.7",
|
||||
"shlinkio/shlink-common": "^5.7.1",
|
||||
"shlinkio/shlink-config": "^2.5",
|
||||
"shlinkio/shlink-event-dispatcher": "^3.1",
|
||||
"shlinkio/shlink-importer": "^5.2.1",
|
||||
"shlinkio/shlink-installer": "^8.6.1",
|
||||
"shlinkio/shlink-ip-geolocation": "^3.4",
|
||||
"shlinkio/doctrine-specification": "^2.1.1",
|
||||
"shlinkio/shlink-common": "^6.3",
|
||||
"shlinkio/shlink-config": "^3.0",
|
||||
"shlinkio/shlink-event-dispatcher": "^4.1",
|
||||
"shlinkio/shlink-importer": "^5.3.2",
|
||||
"shlinkio/shlink-installer": "^9.2",
|
||||
"shlinkio/shlink-ip-geolocation": "^4.0",
|
||||
"shlinkio/shlink-json": "^1.1",
|
||||
"spiral/roadrunner": "^2023.2",
|
||||
"spiral/roadrunner-cli": "^2.5",
|
||||
"spiral/roadrunner-http": "^3.1",
|
||||
"spiral/roadrunner-jobs": "^4.0",
|
||||
"symfony/console": "^6.3",
|
||||
"symfony/filesystem": "^6.3",
|
||||
"symfony/lock": "^6.3",
|
||||
"symfony/process": "^6.3",
|
||||
"symfony/string": "^6.3"
|
||||
"spiral/roadrunner": "^2024.1",
|
||||
"spiral/roadrunner-cli": "^2.6",
|
||||
"spiral/roadrunner-http": "^3.5",
|
||||
"spiral/roadrunner-jobs": "^4.5",
|
||||
"symfony/console": "^7.0",
|
||||
"symfony/filesystem": "^7.0",
|
||||
"symfony/lock": "^7.0",
|
||||
"symfony/process": "^7.0",
|
||||
"symfony/string": "^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"devizzent/cebe-php-openapi": "^1.0.1",
|
||||
"devster/ubench": "^2.1",
|
||||
"infection/infection": "^0.27",
|
||||
"openswoole/ide-helper": "~22.0.0",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpstan/phpstan-doctrine": "^1.3",
|
||||
"phpstan/phpstan-phpunit": "^1.3",
|
||||
"phpstan/phpstan-symfony": "^1.3",
|
||||
"phpunit/php-code-coverage": "^10.1",
|
||||
"phpunit/phpunit": "^10.4",
|
||||
"phpstan/phpstan": "^1.11",
|
||||
"phpstan/phpstan-doctrine": "^1.4",
|
||||
"phpstan/phpstan-phpunit": "^1.4",
|
||||
"phpstan/phpstan-symfony": "^1.4",
|
||||
"phpunit/php-code-coverage": "^11.0",
|
||||
"phpunit/phpcov": "^10.0",
|
||||
"phpunit/phpunit": "^11.3",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~2.3.0",
|
||||
"shlinkio/shlink-test-utils": "^3.8",
|
||||
"symfony/var-dumper": "^6.3",
|
||||
"shlinkio/shlink-test-utils": "^4.1",
|
||||
"symfony/var-dumper": "^7.0",
|
||||
"veewee/composer-run-parallel": "^1.3"
|
||||
},
|
||||
"conflict": {
|
||||
@@ -111,65 +109,42 @@
|
||||
},
|
||||
"scripts": {
|
||||
"ci": [
|
||||
"@parallel cs stan swagger:validate test:unit:ci test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
|
||||
"@parallel infect:test:api infect:test:cli infect:ci:unit infect:ci:db"
|
||||
"@parallel cs stan swagger:validate test:unit:ci test:db:sqlite:ci test:db:postgres test:db:mysql test:db:maria test:db:ms",
|
||||
"@parallel test:api:ci test:cli:ci"
|
||||
],
|
||||
"cs": "phpcs -s",
|
||||
"cs:fix": "phpcbf",
|
||||
"stan": "APP_ENV=test php vendor/bin/phpstan analyse module/*/src module/*/test* module/*/config config docker/config data/migrations --level=8",
|
||||
"stan": "APP_ENV=test php vendor/bin/phpstan analyse",
|
||||
"test": [
|
||||
"@parallel test:unit test:db",
|
||||
"@parallel test:api test:cli"
|
||||
],
|
||||
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --testdox",
|
||||
"test:unit:ci": "@test:unit --coverage-php=build/coverage-unit.cov --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
|
||||
"test:unit": "COLUMNS=120 vendor/bin/phpunit --order-by=random --testdox --testdox-summary",
|
||||
"test:unit:ci": "@test:unit --coverage-php=build/coverage-unit.cov",
|
||||
"test:unit:pretty": "@test:unit --coverage-html build/coverage-unit/coverage-html",
|
||||
"test:db": "@parallel test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
|
||||
"test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml",
|
||||
"test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov --coverage-xml=build/coverage-db/coverage-xml --log-junit=build/coverage-db/junit.xml",
|
||||
"test:db:mysql": "DB_DRIVER=mysql composer test:db:sqlite",
|
||||
"test:db:maria": "DB_DRIVER=maria composer test:db:sqlite",
|
||||
"test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite",
|
||||
"test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite",
|
||||
"test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --testdox --testdox-summary -c phpunit-db.xml",
|
||||
"test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov",
|
||||
"test:db:mysql": "DB_DRIVER=mysql composer test:db:sqlite -- $*",
|
||||
"test:db:maria": "DB_DRIVER=maria composer test:db:sqlite -- $*",
|
||||
"test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite -- $*",
|
||||
"test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite -- $*",
|
||||
"test:api": "bin/test/run-api-tests.sh",
|
||||
"test:api:rr": "TEST_RUNTIME=rr bin/test/run-api-tests.sh",
|
||||
"test:api:ci": "GENERATE_COVERAGE=yes composer test:api",
|
||||
"test:api:pretty": "GENERATE_COVERAGE=pretty composer test:api",
|
||||
"test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml --log-junit=build/coverage-cli/junit.xml",
|
||||
"test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli",
|
||||
"test:cli:pretty": "GENERATE_COVERAGE=pretty composer test:cli",
|
||||
"infect:ci:base": "infection --threads=max --only-covered --skip-initial-tests",
|
||||
"infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=80",
|
||||
"infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json5",
|
||||
"infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=95 --configuration=infection-api.json5",
|
||||
"infect:ci:cli": "@infect:ci:base --coverage=build/coverage-cli --min-msi=90 --configuration=infection-cli.json5",
|
||||
"infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api infect:ci:cli",
|
||||
"infect:test": [
|
||||
"@parallel test:unit:ci test:db:sqlite:ci test:api:ci",
|
||||
"@infect:ci"
|
||||
],
|
||||
"infect:test:unit": [
|
||||
"@test:unit:ci",
|
||||
"@infect:ci:unit"
|
||||
],
|
||||
"infect:test:db": [
|
||||
"@test:db:sqlite:ci",
|
||||
"@infect:ci:db"
|
||||
],
|
||||
"infect:test:api": [
|
||||
"@test:api:ci",
|
||||
"@infect:ci:api"
|
||||
],
|
||||
"infect:test:cli": [
|
||||
"@test:cli:ci",
|
||||
"@infect:ci:cli"
|
||||
],
|
||||
"test:api:sqlite": "DB_DRIVER=sqlite composer test:api -- $*",
|
||||
"test:api:mysql": "DB_DRIVER=mysql composer test:api -- $*",
|
||||
"test:api:maria": "DB_DRIVER=maria composer test:api -- $*",
|
||||
"test:api:mssql": "DB_DRIVER=mssql composer test:api -- $*",
|
||||
"test:api:ci": "GENERATE_COVERAGE=yes composer test:api && vendor/bin/phpcov merge build/coverage-api --php build/coverage-api.cov && rm build/coverage-api/*.cov",
|
||||
"test:api:pretty": "GENERATE_COVERAGE=yes composer test:api && vendor/bin/phpcov merge build/coverage-api --html build/coverage-api/coverage-html && rm build/coverage-api/*.cov",
|
||||
"test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --testdox --testdox-summary -c phpunit-cli.xml",
|
||||
"test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli && vendor/bin/phpcov merge build/coverage-cli --php build/coverage-cli.cov && rm build/coverage-cli/*.cov",
|
||||
"test:cli:pretty": "GENERATE_COVERAGE=yes composer test:cli && vendor/bin/phpcov merge build/coverage-cli --html build/coverage-cli/coverage-html && rm build/coverage-cli/*.cov",
|
||||
"swagger:validate": "php-openapi validate docs/swagger/swagger.json",
|
||||
"swagger:inline": "php-openapi inline docs/swagger/swagger.json docs/swagger/swagger-inlined.json",
|
||||
"clean:dev": "rm -f data/database.sqlite && rm -f config/params/generated_config.php"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"ci": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"swagger:validate\", \"test:ci\" and \"infect:ci\"</>",
|
||||
"ci": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"swagger:validate\" and \"test:ci\"</>",
|
||||
"cs": "<fg=blue;options=bold>Checks coding styles</>",
|
||||
"cs:fix": "<fg=blue;options=bold>Fixes coding styles, when possible</>",
|
||||
"stan": "<fg=blue;options=bold>Inspects code with phpstan</>",
|
||||
@@ -190,10 +165,6 @@
|
||||
"test:cli": "<fg=blue;options=bold>Runs CLI test suites</>",
|
||||
"test:cli:ci": "<fg=blue;options=bold>Runs CLI test suites, and generates code coverage for CI</>",
|
||||
"test:cli:pretty": "<fg=blue;options=bold>Runs CLI test suites, and generates code coverage in HTML format</>",
|
||||
"infect:ci": "<fg=blue;options=bold>Checks unit and db tests quality applying mutation testing with existing reports and logs</>",
|
||||
"infect:ci:unit": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing with existing reports and logs</>",
|
||||
"infect:ci:db": "<fg=blue;options=bold>Checks db tests quality applying mutation testing with existing reports and logs</>",
|
||||
"infect:test": "<fg=blue;options=bold>Runs unit and db tests, then checks tests quality applying mutation testing</>",
|
||||
"swagger:validate": "<fg=blue;options=bold>Validates the swagger docs, making sure they fulfil the spec</>",
|
||||
"swagger:inline": "<fg=blue;options=bold>Inlines swagger docs in a single file</>",
|
||||
"clean:dev": "<fg=blue;options=bold>Deletes artifacts which are gitignored and could affect dev env</>"
|
||||
@@ -204,7 +175,6 @@
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true,
|
||||
"infection/extension-installer": true,
|
||||
"veewee/composer-run-parallel": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ return (static function (): array {
|
||||
'redis' => [
|
||||
'servers' => $redisServers,
|
||||
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
|
||||
'decode_credentials' => (bool) EnvVars::REDIS_DECODE_CREDENTIALS->loadFromEnv(false),
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ return [
|
||||
|
||||
'debug' => false,
|
||||
|
||||
// Disabling config cache for cli, ensures it's never used for openswoole/RoadRunner, and also that console
|
||||
// Disabling config cache for cli, ensures it's never used for RoadRunner, and also that console
|
||||
// commands don't generate a cache file that's then used by php-fpm web executions
|
||||
ConfigAggregator::ENABLE_CACHE => PHP_SAPI !== 'cli',
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use Mezzio\Application;
|
||||
use Mezzio\Container;
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
@@ -12,12 +13,14 @@ use Psr\Http\Message\StreamFactoryInterface;
|
||||
use Psr\Http\Message\UploadedFileFactoryInterface;
|
||||
use Spiral\RoadRunner\Http\PSR7Worker;
|
||||
use Spiral\RoadRunner\WorkerInterface;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
PSR7Worker::class => ConfigAbstractFactory::class,
|
||||
Filesystem::class => InvokableFactory::class,
|
||||
],
|
||||
|
||||
'delegators' => [
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\Events;
|
||||
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use Shlinkio\Shlink\Core\Visit\Listener\OrphanVisitsCountTracker;
|
||||
use Shlinkio\Shlink\Core\Visit\Listener\ShortUrlVisitsCountTracker;
|
||||
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
|
||||
@@ -16,6 +19,10 @@ return (static function (): array {
|
||||
'mssql' => 'pdo_sqlsrv',
|
||||
default => 'pdo_mysql',
|
||||
};
|
||||
$readCredentialAsString = static function (EnvVars $envVar): string|null {
|
||||
$value = $envVar->loadFromEnv();
|
||||
return $value === null ? null : (string) $value;
|
||||
};
|
||||
$resolveDefaultPort = static fn () => match ($driver) {
|
||||
'postgres' => '5432',
|
||||
'mssql' => '1433',
|
||||
@@ -28,6 +35,7 @@ return (static function (): array {
|
||||
'postgres' => 'utf8',
|
||||
default => null,
|
||||
};
|
||||
|
||||
$resolveConnection = static fn () => match ($driver) {
|
||||
null, 'sqlite' => [
|
||||
'driver' => 'pdo_sqlite',
|
||||
@@ -36,8 +44,8 @@ return (static function (): array {
|
||||
default => [
|
||||
'driver' => $resolveDriver(),
|
||||
'dbname' => EnvVars::DB_NAME->loadFromEnv('shlink'),
|
||||
'user' => EnvVars::DB_USER->loadFromEnv(),
|
||||
'password' => EnvVars::DB_PASSWORD->loadFromEnv(),
|
||||
'user' => $readCredentialAsString(EnvVars::DB_USER),
|
||||
'password' => $readCredentialAsString(EnvVars::DB_PASSWORD),
|
||||
'host' => EnvVars::DB_HOST->loadFromEnv(EnvVars::DB_UNIX_SOCKET->loadFromEnv()),
|
||||
'port' => EnvVars::DB_PORT->loadFromEnv($resolveDefaultPort()),
|
||||
'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET->loadFromEnv() : null,
|
||||
@@ -55,6 +63,10 @@ return (static function (): array {
|
||||
'proxies_dir' => 'data/proxies',
|
||||
'load_mappings_using_functional_style' => true,
|
||||
'default_repository_classname' => EntitySpecificationRepository::class,
|
||||
'listeners' => [
|
||||
Events::onFlush => [ShortUrlVisitsCountTracker::class, OrphanVisitsCountTracker::class],
|
||||
Events::postFlush => [ShortUrlVisitsCountTracker::class, OrphanVisitsCountTracker::class],
|
||||
],
|
||||
],
|
||||
'connection' => $resolveConnection(),
|
||||
],
|
||||
|
||||
@@ -12,6 +12,7 @@ return [
|
||||
'installer' => [
|
||||
'enabled_options' => [
|
||||
Option\Server\RuntimeConfigOption::class,
|
||||
Option\Server\MemoryLimitConfigOption::class,
|
||||
Option\Database\DatabaseDriverConfigOption::class,
|
||||
Option\Database\DatabaseNameConfigOption::class,
|
||||
Option\Database\DatabaseHostConfigOption::class,
|
||||
@@ -21,8 +22,6 @@ return [
|
||||
Option\Database\DatabaseUnixSocketConfigOption::class,
|
||||
Option\UrlShortener\ShortDomainHostConfigOption::class,
|
||||
Option\UrlShortener\ShortDomainSchemaConfigOption::class,
|
||||
Option\Visit\VisitsWebhooksConfigOption::class,
|
||||
Option\Visit\OrphanVisitsWebhooksConfigOption::class,
|
||||
Option\Redirect\BaseUrlRedirectConfigOption::class,
|
||||
Option\Redirect\InvalidShortUrlRedirectConfigOption::class,
|
||||
Option\Redirect\Regular404RedirectConfigOption::class,
|
||||
@@ -30,10 +29,7 @@ return [
|
||||
Option\BasePathConfigOption::class,
|
||||
Option\TimezoneConfigOption::class,
|
||||
Option\Cache\CacheNamespaceConfigOption::class,
|
||||
Option\Worker\TaskWorkerNumConfigOption::class,
|
||||
Option\Worker\WebWorkerNumConfigOption::class,
|
||||
Option\Redis\RedisServersConfigOption::class,
|
||||
Option\Redis\RedisDecodeCredentialsConfigOption::class,
|
||||
Option\Redis\RedisSentinelServiceConfigOption::class,
|
||||
Option\Redis\RedisPubSubConfigOption::class,
|
||||
Option\UrlShortener\ShortCodeLengthOption::class,
|
||||
@@ -49,6 +45,8 @@ return [
|
||||
Option\UrlShortener\EnableMultiSegmentSlugsConfigOption::class,
|
||||
Option\UrlShortener\EnableTrailingSlashConfigOption::class,
|
||||
Option\UrlShortener\ShortUrlModeConfigOption::class,
|
||||
Option\Robots\RobotsAllowAllShortUrlsConfigOption::class,
|
||||
Option\Robots\RobotsUserAgentsConfigOption::class,
|
||||
Option\Tracking\IpAnonymizationConfigOption::class,
|
||||
Option\Tracking\OrphanVisitsTrackingConfigOption::class,
|
||||
Option\Tracking\DisableTrackParamConfigOption::class,
|
||||
@@ -62,6 +60,10 @@ return [
|
||||
Option\QrCode\DefaultFormatConfigOption::class,
|
||||
Option\QrCode\DefaultErrorCorrectionConfigOption::class,
|
||||
Option\QrCode\DefaultRoundBlockSizeConfigOption::class,
|
||||
Option\QrCode\DefaultColorConfigOption::class,
|
||||
Option\QrCode\DefaultBgColorConfigOption::class,
|
||||
Option\QrCode\DefaultLogoUrlConfigOption::class,
|
||||
Option\QrCode\EnabledForDisabledShortUrlsConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqEnabledConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqHostConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqUseSslConfigOption::class,
|
||||
|
||||
@@ -4,23 +4,27 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use Monolog\Level;
|
||||
use Monolog\Logger;
|
||||
use PhpMiddleware\RequestId;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerFactory;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
||||
use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Helper\RequestIdProvider;
|
||||
use Shlinkio\Shlink\EventDispatcher\Util\RequestIdProviderInterface;
|
||||
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
|
||||
return (static function (): array {
|
||||
$common = [
|
||||
'level' => Level::Info->value,
|
||||
'processors' => [RequestId\MonologProcessor::class],
|
||||
'line_format' => '[%datetime%] [%extra.request_id%] %channel%.%level_name% - %message%',
|
||||
'processors' => [RequestIdMiddleware::class],
|
||||
'line_format' =>
|
||||
'[%datetime%] [%extra.' . RequestIdMiddleware::ATTRIBUTE . '%] %channel%.%level_name% - %message%',
|
||||
];
|
||||
|
||||
return [
|
||||
@@ -43,24 +47,19 @@ return (static function (): array {
|
||||
'Logger_Shlink' => [LoggerFactory::class, 'Shlink'],
|
||||
'Logger_Access' => [LoggerFactory::class, 'Access'],
|
||||
NullLogger::class => InvokableFactory::class,
|
||||
RequestIdProvider::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'logger' => 'Logger_Shlink',
|
||||
Logger::class => 'Logger_Shlink',
|
||||
LoggerInterface::class => 'Logger_Shlink',
|
||||
AccessLogMiddleware::LOGGER_SERVICE_NAME => 'Logger_Access',
|
||||
RequestIdProviderInterface::class => RequestIdProvider::class,
|
||||
],
|
||||
],
|
||||
|
||||
// Deprecated. Remove in Shlink 4.0.0
|
||||
'mezzio-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'logger' => [
|
||||
// Let's disable mezio-swoole access logging, so that we can provide our own implementation,
|
||||
// consistent for roadrunner and openswoole
|
||||
'logger-name' => NullLogger::class,
|
||||
],
|
||||
],
|
||||
ConfigAbstractFactory::class => [
|
||||
RequestIdProvider::class => [RequestIdMiddleware::class],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
return [
|
||||
|
||||
'mercure' => [
|
||||
'public_hub_url' => 'http://localhost:8001',
|
||||
'public_hub_url' => 'http://localhost:8002',
|
||||
'internal_hub_url' => 'http://shlink_mercure_proxy',
|
||||
'jwt_secret' => 'mercure_jwt_key_long_enough_to_avoid_error',
|
||||
],
|
||||
|
||||
@@ -7,10 +7,10 @@ namespace Shlinkio\Shlink;
|
||||
use Laminas\Stratigility\Middleware\ErrorHandler;
|
||||
use Mezzio\ProblemDetails;
|
||||
use Mezzio\Router;
|
||||
use PhpMiddleware\RequestId\RequestIdMiddleware;
|
||||
use RKA\Middleware\IpAddress;
|
||||
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
||||
use Shlinkio\Shlink\Common\Middleware\ContentLengthMiddleware;
|
||||
use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware;
|
||||
|
||||
return [
|
||||
|
||||
@@ -47,7 +47,6 @@ return [
|
||||
'rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
Rest\Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class,
|
||||
Router\Middleware\ImplicitOptionsMiddleware::class,
|
||||
Rest\Middleware\BodyParserMiddleware::class,
|
||||
Rest\Middleware\AuthenticationMiddleware::class,
|
||||
|
||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_BG_COLOR;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_COLOR;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_ERROR_CORRECTION;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_MARGIN;
|
||||
@@ -22,6 +25,12 @@ return [
|
||||
'round_block_size' => (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE->loadFromEnv(
|
||||
DEFAULT_QR_CODE_ROUND_BLOCK_SIZE,
|
||||
),
|
||||
'enabled_for_disabled_short_urls' => (bool) EnvVars::QR_CODE_FOR_DISABLED_SHORT_URLS->loadFromEnv(
|
||||
DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS,
|
||||
),
|
||||
'color' => EnvVars::DEFAULT_QR_CODE_COLOR->loadFromEnv(DEFAULT_QR_CODE_COLOR),
|
||||
'bg_color' => EnvVars::DEFAULT_QR_CODE_BG_COLOR->loadFromEnv(DEFAULT_QR_CODE_BG_COLOR),
|
||||
'logo_url' => EnvVars::DEFAULT_QR_CODE_LOGO_URL->loadFromEnv(),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -14,9 +14,6 @@ return [
|
||||
'user' => EnvVars::RABBITMQ_USER->loadFromEnv(),
|
||||
'password' => EnvVars::RABBITMQ_PASSWORD->loadFromEnv(),
|
||||
'vhost' => EnvVars::RABBITMQ_VHOST->loadFromEnv('/'),
|
||||
|
||||
// Deprecated
|
||||
'legacy_visits_publishing' => (bool) EnvVars::RABBITMQ_LEGACY_VISITS_PUBLISHING->loadFromEnv(false),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -7,6 +7,7 @@ return [
|
||||
'rabbitmq' => [
|
||||
'enabled' => true,
|
||||
'host' => 'shlink_rabbitmq',
|
||||
'port' => '5672',
|
||||
'user' => 'rabbit',
|
||||
'password' => 'rabbit',
|
||||
],
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use PhpMiddleware\RequestId;
|
||||
use Shlinkio\Shlink\Common\Logger\Processor\BackwardsCompatibleMonologProcessorDelegator;
|
||||
|
||||
return [
|
||||
|
||||
'request_id' => [
|
||||
'allow_override' => true,
|
||||
'header_name' => 'X-Request-Id',
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
RequestId\Generator\RamseyUuid4StaticGenerator::class => InvokableFactory::class,
|
||||
RequestId\RequestIdProviderFactory::class => ConfigAbstractFactory::class,
|
||||
RequestId\RequestIdMiddleware::class => ConfigAbstractFactory::class,
|
||||
RequestId\MonologProcessor::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
'delegators' => [
|
||||
RequestId\MonologProcessor::class => [
|
||||
BackwardsCompatibleMonologProcessorDelegator::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
RequestId\RequestIdProviderFactory::class => [
|
||||
RequestId\Generator\RamseyUuid4StaticGenerator::class,
|
||||
'config.request_id.allow_override',
|
||||
'config.request_id.header_name',
|
||||
],
|
||||
RequestId\RequestIdMiddleware::class => [
|
||||
RequestId\RequestIdProviderFactory::class,
|
||||
'config.request_id.header_name',
|
||||
],
|
||||
RequestId\MonologProcessor::class => [RequestId\RequestIdMiddleware::class],
|
||||
],
|
||||
|
||||
];
|
||||
14
config/autoload/robots.global.php
Normal file
14
config/autoload/robots.global.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
return [
|
||||
|
||||
'robots' => [
|
||||
'allow-all-short-urls' => (bool) Config\EnvVars::ROBOTS_ALLOW_ALL_SHORT_URLS->loadFromEnv(false),
|
||||
'user-agents' => splitByComma(Config\EnvVars::ROBOTS_USER_AGENTS->loadFromEnv()),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -11,7 +11,9 @@ return [
|
||||
'base_path' => EnvVars::BASE_PATH->loadFromEnv(''),
|
||||
|
||||
'fastroute' => [
|
||||
FastRouteRouter::CONFIG_CACHE_ENABLED => true,
|
||||
// Disabling config cache for cli, ensures it's never used for RoadRunner, and also that console
|
||||
// commands don't generate a cache file that's then used by php-fpm web executions
|
||||
FastRouteRouter::CONFIG_CACHE_ENABLED => PHP_SAPI !== 'cli',
|
||||
FastRouteRouter::CONFIG_CACHE_FILE => 'data/cache/fastroute_cached_routes.php',
|
||||
],
|
||||
],
|
||||
|
||||
@@ -17,7 +17,6 @@ use Shlinkio\Shlink\Rest\Middleware\Mercure\NotConfiguredMercureErrorHandler;
|
||||
use function sprintf;
|
||||
|
||||
return (static function (): array {
|
||||
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
|
||||
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
|
||||
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
||||
|
||||
@@ -32,9 +31,10 @@ return (static function (): array {
|
||||
...ConfigProvider::applyRoutesPrefix([
|
||||
Action\HealthAction::getRouteDef(),
|
||||
|
||||
// Visits and rules routes must go first, as they have a more specific path, otherwise, when
|
||||
// multi-segment slugs are enabled, routes with a less-specific path might match first
|
||||
|
||||
// Visits.
|
||||
// These routes must go first, as they have a more specific path, otherwise, when multi-segment slugs
|
||||
// are enabled, routes with a less-specific path might match first
|
||||
Action\Visit\ShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\Visit\TagVisitsAction::getRouteDef(),
|
||||
@@ -44,15 +44,18 @@ return (static function (): array {
|
||||
Action\Visit\DeleteOrphanVisitsAction::getRouteDef(),
|
||||
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
|
||||
|
||||
//Redirect rules
|
||||
Action\RedirectRule\ListRedirectRulesAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\RedirectRule\SetRedirectRulesAction::getRouteDef([$dropDomainMiddleware]),
|
||||
|
||||
// Short URLs
|
||||
Action\ShortUrl\CreateShortUrlAction::getRouteDef([
|
||||
$contentNegotiationMiddleware,
|
||||
$dropDomainMiddleware,
|
||||
$overrideDomainMiddleware,
|
||||
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class,
|
||||
]),
|
||||
Action\ShortUrl\SingleStepCreateShortUrlAction::getRouteDef([
|
||||
$contentNegotiationMiddleware,
|
||||
Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class,
|
||||
$overrideDomainMiddleware,
|
||||
]),
|
||||
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
use function Shlinkio\Shlink\Config\getOpenswooleConfigFromEnv;
|
||||
|
||||
use const Shlinkio\Shlink\MIN_TASK_WORKERS;
|
||||
|
||||
return (static function (): array {
|
||||
$taskWorkers = (int) EnvVars::TASK_WORKER_NUM->loadFromEnv(16);
|
||||
|
||||
return [
|
||||
|
||||
'mezzio-swoole' => [
|
||||
// Setting this to true can have unexpected behaviors when running several concurrent slow DB queries
|
||||
'enable_coroutine' => false,
|
||||
|
||||
'swoole-http-server' => [
|
||||
'host' => '0.0.0.0',
|
||||
'port' => (int) EnvVars::PORT->loadFromEnv(8080),
|
||||
'process-name' => 'shlink',
|
||||
|
||||
'options' => [
|
||||
...getOpenswooleConfigFromEnv(),
|
||||
'worker_num' => (int) EnvVars::WEB_WORKER_NUM->loadFromEnv(16),
|
||||
'task_worker_num' => max($taskWorkers, MIN_TASK_WORKERS),
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
})();
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'mezzio-swoole' => [
|
||||
'hot-code-reload' => [
|
||||
'enable' => true,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -4,40 +4,35 @@ declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return (static function (): array {
|
||||
/** @var string|null $disableTrackingFrom */
|
||||
$disableTrackingFrom = EnvVars::DISABLE_TRACKING_FROM->loadFromEnv();
|
||||
use function Shlinkio\Shlink\Core\splitByComma;
|
||||
|
||||
return [
|
||||
return [
|
||||
|
||||
'tracking' => [
|
||||
// Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
|
||||
// This applies only if IP address tracking is enabled
|
||||
'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(true),
|
||||
'tracking' => [
|
||||
// Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
|
||||
// This applies only if IP address tracking is enabled
|
||||
'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(true),
|
||||
|
||||
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
|
||||
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true),
|
||||
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
|
||||
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true),
|
||||
|
||||
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
|
||||
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(),
|
||||
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
|
||||
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(),
|
||||
|
||||
// If true, visits will not be tracked at all
|
||||
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false),
|
||||
// If true, visits will not be tracked at all
|
||||
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false),
|
||||
|
||||
// If true, visits will be tracked, but neither the IP address, nor the location will be resolved
|
||||
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false),
|
||||
// If true, visits will be tracked, but neither the IP address, nor the location will be resolved
|
||||
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false),
|
||||
|
||||
// If true, the referrer will not be tracked
|
||||
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false),
|
||||
// If true, the referrer will not be tracked
|
||||
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false),
|
||||
|
||||
// If true, the user agent will not be tracked
|
||||
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false),
|
||||
// If true, the user agent will not be tracked
|
||||
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false),
|
||||
|
||||
// A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
|
||||
'disable_tracking_from' => $disableTrackingFrom === null
|
||||
? []
|
||||
: array_map(trim(...), explode(',', $disableTrackingFrom)),
|
||||
],
|
||||
// A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
|
||||
'disable_tracking_from' => splitByComma(EnvVars::DISABLE_TRACKING_FROM->loadFromEnv()),
|
||||
],
|
||||
|
||||
];
|
||||
})();
|
||||
];
|
||||
|
||||
@@ -14,7 +14,7 @@ return (static function (): array {
|
||||
MIN_SHORT_CODES_LENGTH,
|
||||
);
|
||||
$modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv(ShortUrlMode::STRICT->value);
|
||||
$mode = ShortUrlMode::tryDeprecated($modeFromEnv) ?? ShortUrlMode::STRICT;
|
||||
$mode = ShortUrlMode::tryFrom($modeFromEnv) ?? ShortUrlMode::STRICT;
|
||||
|
||||
return [
|
||||
|
||||
@@ -24,7 +24,7 @@ return (static function (): array {
|
||||
'hostname' => EnvVars::DEFAULT_DOMAIN->loadFromEnv(''),
|
||||
],
|
||||
'default_short_codes_length' => $shortCodesLength,
|
||||
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(false),
|
||||
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(true),
|
||||
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(false),
|
||||
'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(false),
|
||||
'trailing_slash_enabled' => (bool) EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Shlinkio\Shlink\Config\runningInOpenswoole;
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
|
||||
return [
|
||||
@@ -12,11 +11,9 @@ return [
|
||||
'schema' => 'http',
|
||||
'hostname' => sprintf('localhost:%s', match (true) {
|
||||
runningInRoadRunner() => '8800',
|
||||
runningInOpenswoole() => '8080',
|
||||
default => '8000',
|
||||
}),
|
||||
],
|
||||
'auto_resolve_titles' => true,
|
||||
// 'multi_segment_slugs_enabled' => true,
|
||||
// 'trailing_slash_enabled' => true,
|
||||
],
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
// Deprecated. Webhooks are no longer supported. To be removed in Shlink 4.0.0
|
||||
return (static function (): array {
|
||||
$webhooks = EnvVars::VISITS_WEBHOOKS->loadFromEnv();
|
||||
|
||||
return [
|
||||
|
||||
'visits_webhooks' => [
|
||||
'webhooks' => $webhooks === null ? [] : explode(',', $webhooks),
|
||||
'notify_orphan_visits_to_webhooks' =>
|
||||
(bool) EnvVars::NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS->loadFromEnv(false),
|
||||
],
|
||||
|
||||
];
|
||||
})();
|
||||
@@ -11,7 +11,7 @@ use Doctrine\Migrations\DependencyFactory;
|
||||
return (static function () {
|
||||
$migrationsConfig = [
|
||||
'migrations_paths' => [
|
||||
'ShlinkMigrations' => 'data/migrations',
|
||||
'ShlinkMigrations' => 'module/Core/migrations',
|
||||
],
|
||||
'table_storage' => [
|
||||
'table_name' => 'migrations',
|
||||
|
||||
@@ -8,19 +8,12 @@ use Laminas\ConfigAggregator;
|
||||
use Laminas\Diactoros;
|
||||
use Mezzio;
|
||||
use Mezzio\ProblemDetails;
|
||||
use Mezzio\Swoole;
|
||||
use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider;
|
||||
|
||||
use function class_exists;
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
use function Shlinkio\Shlink\Config\openswooleIsInstalled;
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
use function Shlinkio\Shlink\Core\enumValues;
|
||||
|
||||
use const PHP_SAPI;
|
||||
|
||||
$isTestEnv = env('APP_ENV') === 'test';
|
||||
$enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoadRunner();
|
||||
|
||||
return (new ConfigAggregator\ConfigAggregator(
|
||||
providers: [
|
||||
@@ -30,9 +23,6 @@ return (new ConfigAggregator\ConfigAggregator(
|
||||
Mezzio\ConfigProvider::class,
|
||||
Mezzio\Router\ConfigProvider::class,
|
||||
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
|
||||
$enableSwoole && class_exists(Swoole\ConfigProvider::class)
|
||||
? Swoole\ConfigProvider::class
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
ProblemDetails\ConfigProvider::class,
|
||||
Diactoros\ConfigProvider::class,
|
||||
Common\ConfigProvider::class,
|
||||
|
||||
@@ -9,14 +9,15 @@ use Shlinkio\Shlink\Core\Util\RedirectStatus;
|
||||
const DEFAULT_DELETE_SHORT_URL_THRESHOLD = 15;
|
||||
const DEFAULT_SHORT_CODES_LENGTH = 5;
|
||||
const MIN_SHORT_CODES_LENGTH = 4;
|
||||
const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302; // Deprecated. Default to 307 for Shlink v4
|
||||
const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302;
|
||||
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
|
||||
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
|
||||
const TITLE_TAG_VALUE = '/<title[^>]*>(.*?)<\/title>/i'; // Matches the value inside a html title tag
|
||||
const LOOSE_URI_MATCHER = '/(.+)\:(.+)/i'; // Matches anything starting with a schema.
|
||||
const DEFAULT_QR_CODE_SIZE = 300;
|
||||
const DEFAULT_QR_CODE_MARGIN = 0;
|
||||
const DEFAULT_QR_CODE_FORMAT = 'png';
|
||||
const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l';
|
||||
const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true;
|
||||
const MIN_TASK_WORKERS = 4;
|
||||
const DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS = true;
|
||||
const DEFAULT_QR_CODE_COLOR = '#000000'; // Black
|
||||
const DEFAULT_QR_CODE_BG_COLOR = '#ffffff'; // White
|
||||
|
||||
@@ -12,17 +12,6 @@ chdir(dirname(__DIR__));
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
// Workaround to make this compatible with both openswoole 22 and earlier versions.
|
||||
// Openswoole support is deprecated. Remove in v4.0.0
|
||||
if (! function_exists('swoole_set_process_name')) {
|
||||
// phpcs:disable
|
||||
function swoole_set_process_name(string $name): void
|
||||
{
|
||||
OpenSwoole\Util::setProcessName($name);
|
||||
}
|
||||
// phpcs:enable
|
||||
}
|
||||
|
||||
// This is one of the first files loaded. Configure the timezone here
|
||||
date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv(date_default_timezone_get()));
|
||||
|
||||
@@ -34,6 +23,10 @@ if (! class_exists(LOCAL_LOCK_FACTORY)) {
|
||||
|
||||
return (static function (): ServiceManager {
|
||||
$config = require __DIR__ . '/config.php';
|
||||
|
||||
// Set memory limit right after loading config, to ensure installer config has been promoted as env vars
|
||||
ini_set('memory_limit', EnvVars::MEMORY_LIMIT->loadFromEnv('512M'));
|
||||
|
||||
$container = new ServiceManager($config['dependencies']);
|
||||
$container->setService('config', $config);
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ logs:
|
||||
http:
|
||||
mode: 'off' # Disable logging as Shlink handles it internally
|
||||
server:
|
||||
level: debug
|
||||
level: info
|
||||
metrics:
|
||||
level: debug
|
||||
jobs:
|
||||
|
||||
49
config/roadrunner/.rr.test.yml
Normal file
49
config/roadrunner/.rr.test.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
version: '3'
|
||||
|
||||
############################################################################################
|
||||
# Routes here need to be relative to the project root, as API tests are run with `-w .` #
|
||||
# See https://github.com/orgs/roadrunner-server/discussions/1440#discussioncomment-8486186 #
|
||||
############################################################################################
|
||||
|
||||
rpc:
|
||||
listen: tcp://127.0.0.1:6001
|
||||
|
||||
server:
|
||||
command: 'php ./bin/roadrunner-worker.php'
|
||||
|
||||
http:
|
||||
address: '0.0.0.0:9999'
|
||||
middleware: ['static']
|
||||
static:
|
||||
dir: './public'
|
||||
forbid: ['.php', '.htaccess']
|
||||
pool:
|
||||
num_workers: 1
|
||||
debug: false
|
||||
|
||||
jobs:
|
||||
pool:
|
||||
num_workers: 1
|
||||
debug: false
|
||||
timeout: 300
|
||||
consume: ['shlink']
|
||||
pipelines:
|
||||
shlink:
|
||||
driver: memory
|
||||
config:
|
||||
priority: 10
|
||||
prefetch: 10
|
||||
|
||||
logs:
|
||||
encoding: json
|
||||
mode: development
|
||||
channels:
|
||||
http:
|
||||
mode: 'off' # Disable logging as Shlink handles it internally
|
||||
server:
|
||||
encoding: json
|
||||
level: info
|
||||
metrics:
|
||||
level: panic
|
||||
jobs:
|
||||
level: panic
|
||||
@@ -7,12 +7,6 @@ namespace Shlinkio\Shlink\TestUtils;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
use function register_shutdown_function;
|
||||
use function sprintf;
|
||||
|
||||
use const ShlinkioTest\Shlink\API_TESTS_HOST;
|
||||
use const ShlinkioTest\Shlink\API_TESTS_PORT;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = require __DIR__ . '/../container.php';
|
||||
$testHelper = $container->get(Helper\TestHelper::class);
|
||||
@@ -20,19 +14,11 @@ $config = $container->get('config');
|
||||
$em = $container->get(EntityManager::class);
|
||||
$httpClient = $container->get('shlink_test_api_client');
|
||||
|
||||
// Dump code coverage when process shuts down
|
||||
register_shutdown_function(function () use ($httpClient): void {
|
||||
$httpClient->request(
|
||||
'GET',
|
||||
sprintf('http://%s:%s/api-tests/stop-coverage', API_TESTS_HOST, API_TESTS_PORT),
|
||||
);
|
||||
});
|
||||
|
||||
$testHelper->createTestDb(
|
||||
['bin/cli', 'db:create'],
|
||||
['bin/cli', 'db:migrate'],
|
||||
['bin/doctrine', 'orm:schema-tool:drop'],
|
||||
['bin/doctrine', 'dbal:run-sql'],
|
||||
createDbCommand: ['bin/cli', 'db:create'],
|
||||
migrateDbCommand: ['bin/cli', 'db:migrate'],
|
||||
dropSchemaCommand: ['bin/doctrine', 'orm:schema-tool:drop'],
|
||||
runSqlCommand: ['bin/doctrine', 'dbal:run-sql'],
|
||||
);
|
||||
ApiTest\ApiTestCase::setApiClient($httpClient);
|
||||
ApiTest\ApiTestCase::setSeedFixturesCallback(fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []));
|
||||
|
||||
@@ -9,9 +9,9 @@ use Psr\Container\ContainerInterface;
|
||||
/** @var ContainerInterface $container */
|
||||
$container = require __DIR__ . '/../container.php';
|
||||
$container->get(Helper\TestHelper::class)->createTestDb(
|
||||
['bin/cli', 'db:create'],
|
||||
['bin/cli', 'db:migrate'],
|
||||
['bin/doctrine', 'orm:schema-tool:drop'],
|
||||
['bin/doctrine', 'dbal:run-sql'],
|
||||
createDbCommand: ['bin/cli', 'db:create'],
|
||||
migrateDbCommand: ['bin/cli', 'db:migrate'],
|
||||
dropSchemaCommand: ['bin/doctrine', 'orm:schema-tool:drop'],
|
||||
runSqlCommand: ['bin/doctrine', 'dbal:run-sql'],
|
||||
);
|
||||
DbTest\DatabaseTestCase::setEntityManager($container->get('em'));
|
||||
|
||||
@@ -6,75 +6,38 @@ namespace Shlinkio\Shlink;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Laminas\ConfigAggregator\ConfigAggregator;
|
||||
use Laminas\Diactoros\Response\EmptyResponse;
|
||||
use Laminas\Diactoros\Response\HtmlResponse;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use League\Event\EventDispatcher;
|
||||
use Mezzio\Router\FastRouteRouter;
|
||||
use Monolog\Level;
|
||||
use PHPUnit\Runner\Version;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\Driver\Selector;
|
||||
use SebastianBergmann\CodeCoverage\Filter;
|
||||
use SebastianBergmann\CodeCoverage\Report\Html\Facade as Html;
|
||||
use SebastianBergmann\CodeCoverage\Report\PHP;
|
||||
use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||
use Shlinkio\Shlink\TestUtils\ApiTest\CoverageMiddleware;
|
||||
use Shlinkio\Shlink\TestUtils\CliTest\CliCoverageDelegator;
|
||||
use Shlinkio\Shlink\TestUtils\Helper\CoverageHelper;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Event\ConsoleCommandEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
use function file_exists;
|
||||
use function Laminas\Stratigility\middleware;
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
use function sleep;
|
||||
use function sprintf;
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
use const ShlinkioTest\Shlink\API_TESTS_HOST;
|
||||
use const ShlinkioTest\Shlink\API_TESTS_PORT;
|
||||
|
||||
$isApiTest = env('TEST_ENV') === 'api';
|
||||
$isCliTest = env('TEST_ENV') === 'cli';
|
||||
$testEnv = env('TEST_ENV');
|
||||
$isApiTest = $testEnv === 'api';
|
||||
$isCliTest = $testEnv === 'cli';
|
||||
$isE2eTest = $isApiTest || $isCliTest;
|
||||
|
||||
$coverageType = env('GENERATE_COVERAGE');
|
||||
$generateCoverage = contains($coverageType, ['yes', 'pretty']);
|
||||
|
||||
$coverage = null;
|
||||
if ($isE2eTest && $generateCoverage) {
|
||||
$filter = new Filter();
|
||||
$filter->includeDirectory(__DIR__ . '/../../module/Core/src');
|
||||
$filter->includeDirectory(__DIR__ . '/../../module/' . ($isApiTest ? 'Rest' : 'CLI') . '/src');
|
||||
$coverage = new CodeCoverage((new Selector())->forLineCoverage($filter), $filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 'api'|'cli' $type
|
||||
*/
|
||||
$exportCoverage = static function (string $type = 'api') use (&$coverage, $coverageType): void {
|
||||
if ($coverage === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$basePath = __DIR__ . '/../../build/coverage-' . $type;
|
||||
$covPath = $basePath . '.cov';
|
||||
|
||||
// Every CLI test runs on its own process and dumps the coverage afterwards.
|
||||
// Try to load it and merge it, so that we end up with the whole coverage at the end.
|
||||
if ($type === 'cli' && file_exists($covPath)) {
|
||||
$coverage->merge(require $covPath);
|
||||
}
|
||||
|
||||
if ($coverageType === 'pretty') {
|
||||
(new Html())->process($coverage, $basePath . '/coverage-html');
|
||||
} else {
|
||||
(new PHP())->process($coverage, $covPath);
|
||||
(new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml');
|
||||
}
|
||||
};
|
||||
$generateCoverage = $coverageType === 'yes';
|
||||
$coverage = $isE2eTest && $generateCoverage ? CoverageHelper::createCoverageForDirectories(
|
||||
[
|
||||
__DIR__ . '/../../module/Core/src',
|
||||
__DIR__ . '/../../module/' . ($isApiTest ? 'Rest' : 'CLI') . '/src',
|
||||
],
|
||||
__DIR__ . '/../../build/coverage-' . $testEnv,
|
||||
) : null;
|
||||
|
||||
$buildDbConnection = static function (): array {
|
||||
$driver = env('DB_DRIVER', 'sqlite');
|
||||
@@ -89,7 +52,7 @@ $buildDbConnection = static function (): array {
|
||||
'postgres' => [
|
||||
'driver' => 'pdo_pgsql',
|
||||
'host' => $isCi ? '127.0.0.1' : 'shlink_db_postgres',
|
||||
'port' => $isCi ? '5433' : '5432',
|
||||
'port' => $isCi ? '5434' : '5432',
|
||||
'user' => 'postgres',
|
||||
'password' => 'root',
|
||||
'dbname' => 'shlink_test',
|
||||
@@ -128,6 +91,7 @@ return [
|
||||
|
||||
'debug' => true,
|
||||
ConfigAggregator::ENABLE_CACHE => false,
|
||||
FastRouteRouter::CONFIG_CACHE_ENABLED => false,
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
@@ -136,52 +100,27 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'mezzio-swoole' => [
|
||||
'enable_coroutine' => false,
|
||||
'swoole-http-server' => [
|
||||
'host' => API_TESTS_HOST,
|
||||
'port' => API_TESTS_PORT,
|
||||
'process-name' => 'shlink_test',
|
||||
'options' => [
|
||||
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
|
||||
'log_file' => __DIR__ . '/../../data/log/api-tests/output.log',
|
||||
'enable_coroutine' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'routes' => !$isApiTest ? [] : [
|
||||
'routes' => [
|
||||
// This route is used to test that title resolution is skipped if the long URL times out
|
||||
[
|
||||
'name' => 'dump_coverage',
|
||||
'path' => '/api-tests/stop-coverage',
|
||||
'middleware' => middleware(static function () use ($exportCoverage) {
|
||||
// TODO I have tried moving this block to a listener so that it's invoked automatically,
|
||||
// but then the coverage is generated empty ¯\_(ツ)_/¯
|
||||
$exportCoverage();
|
||||
return new EmptyResponse();
|
||||
}),
|
||||
'name' => 'long_url_with_timeout',
|
||||
'path' => '/api-tests/long-url-with-timeout',
|
||||
'allowed_methods' => ['GET'],
|
||||
'middleware' => middleware(static function () {
|
||||
sleep(5); // Title resolution times out at 3 seconds
|
||||
return new HtmlResponse('<title>The title</title>');
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
'middleware_pipeline' => !$isApiTest ? [] : [
|
||||
'capture_code_coverage' => [
|
||||
'middleware' => middleware(static function (
|
||||
ServerRequestInterface $req,
|
||||
RequestHandlerInterface $handler,
|
||||
) use (&$coverage): ResponseInterface {
|
||||
$coverage?->start($req->getHeaderLine('x-coverage-id'));
|
||||
|
||||
try {
|
||||
return $handler->handle($req);
|
||||
} finally {
|
||||
$coverage?->stop();
|
||||
}
|
||||
}),
|
||||
'middleware' => new CoverageMiddleware($coverage),
|
||||
'priority' => 9999,
|
||||
],
|
||||
],
|
||||
|
||||
// Disable mercure integration during E2E tests
|
||||
'mercure' => [
|
||||
'public_hub_url' => null,
|
||||
'internal_hub_url' => null,
|
||||
@@ -200,58 +139,7 @@ return [
|
||||
],
|
||||
'delegators' => $isCliTest ? [
|
||||
Application::class => [
|
||||
static function (
|
||||
ContainerInterface $c,
|
||||
string $serviceName,
|
||||
callable $callback,
|
||||
) use (
|
||||
&$coverage,
|
||||
$exportCoverage,
|
||||
) {
|
||||
/** @var Application $app */
|
||||
$app = $callback();
|
||||
$wrappedEventDispatcher = new EventDispatcher();
|
||||
|
||||
// When the command starts, start collecting coverage
|
||||
$wrappedEventDispatcher->subscribeTo(
|
||||
ConsoleCommandEvent::class,
|
||||
static function () use (&$coverage): void {
|
||||
$id = env('COVERAGE_ID');
|
||||
if ($id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$coverage?->start($id);
|
||||
},
|
||||
);
|
||||
// When the command ends, stop collecting coverage
|
||||
$wrappedEventDispatcher->subscribeTo(
|
||||
ConsoleTerminateEvent::class,
|
||||
static function () use (&$coverage, $exportCoverage): void {
|
||||
$id = env('COVERAGE_ID');
|
||||
if ($id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$coverage?->stop();
|
||||
$exportCoverage('cli');
|
||||
},
|
||||
);
|
||||
|
||||
$app->setDispatcher(new class ($wrappedEventDispatcher) implements EventDispatcherInterface {
|
||||
public function __construct(private EventDispatcher $wrappedDispatcher)
|
||||
{
|
||||
}
|
||||
|
||||
public function dispatch(object $event, ?string $eventName = null): object
|
||||
{
|
||||
$this->wrappedDispatcher->dispatch($event);
|
||||
return $event;
|
||||
}
|
||||
});
|
||||
|
||||
return $app;
|
||||
},
|
||||
new CliCoverageDelegator($coverage),
|
||||
],
|
||||
] : [],
|
||||
],
|
||||
@@ -262,7 +150,7 @@ return [
|
||||
|
||||
'data_fixtures' => [
|
||||
'paths' => [
|
||||
// TODO These are used for CLI tests too, so maybe should be somewhere else
|
||||
// TODO These are used for other module's tests, so maybe should be somewhere else
|
||||
__DIR__ . '/../../module/Rest/test-api/Fixtures',
|
||||
],
|
||||
],
|
||||
|
||||
@@ -11,7 +11,7 @@ server {
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
|
||||
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/var/log/shlink/shlink_openswoole.log {
|
||||
su root root
|
||||
daily
|
||||
missingok
|
||||
rotate 120
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 0640 root root
|
||||
postrotate
|
||||
/etc/init.d/shlink_openswoole restart
|
||||
endscript
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/bin/bash
|
||||
### BEGIN INIT INFO
|
||||
# Provides: shlink_openswoole
|
||||
# Required-Start: $local_fs $network $named $time $syslog
|
||||
# Required-Stop: $local_fs $network $named $time $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Description: Shlink non-blocking server with openswoole
|
||||
### END INIT INFO
|
||||
|
||||
SCRIPT=/path/to/shlink/vendor/bin/laminas\ mezzio:swoole:start
|
||||
RUNAS=root
|
||||
|
||||
PIDFILE=/var/run/shlink_openswoole.pid
|
||||
LOGDIR=/var/log/shlink
|
||||
LOGFILE=${LOGDIR}/shlink_openswoole.log
|
||||
|
||||
start() {
|
||||
if [[ -f "$PIDFILE" ]] && kill -0 $(cat "$PIDFILE"); then
|
||||
echo 'Shlink with openswoole already running' >&2
|
||||
return 1
|
||||
fi
|
||||
echo 'Starting shlink with openswoole' >&2
|
||||
mkdir -p "$LOGDIR"
|
||||
touch "$LOGFILE"
|
||||
local CMD="$SCRIPT &> \"$LOGFILE\" & echo \$!"
|
||||
su -c "$CMD" $RUNAS > "$PIDFILE"
|
||||
echo 'Shlink started' >&2
|
||||
}
|
||||
|
||||
stop() {
|
||||
if [[ ! -f "$PIDFILE" ]] || ! kill -0 $(cat "$PIDFILE"); then
|
||||
echo 'Shlink with openswoole not running' >&2
|
||||
return 1
|
||||
fi
|
||||
echo 'Stopping shlink with openswoole' >&2
|
||||
kill -15 $(cat "$PIDFILE") && rm -f "$PIDFILE"
|
||||
echo 'Shlink stopped' >&2
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
stop
|
||||
start
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
esac
|
||||
@@ -1,8 +1,8 @@
|
||||
FROM php:8.2-fpm-alpine3.17
|
||||
FROM php:8.3-fpm-alpine3.19
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.21
|
||||
ENV PDO_SQLSRV_VERSION 5.11.1
|
||||
ENV APCU_VERSION 5.1.23
|
||||
ENV PDO_SQLSRV_VERSION 5.12.0
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
display_errors=On
|
||||
error_reporting=-1
|
||||
memory_limit=-1
|
||||
log_errors_max_len=0
|
||||
zend.assertions=1
|
||||
assert.exception=1
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
FROM php:8.2-alpine3.17
|
||||
FROM php:8.3-alpine3.19
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.21
|
||||
ENV PDO_SQLSRV_VERSION 5.11.1
|
||||
ENV APCU_VERSION 5.1.23
|
||||
ENV PDO_SQLSRV_VERSION 5.12.0
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
FROM php:8.2-alpine3.17
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.21
|
||||
ENV INOTIFY_VERSION 3.0.0
|
||||
ENV OPENSWOOLE_VERSION 22.1.0
|
||||
ENV PDO_SQLSRV_VERSION 5.11.1
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
|
||||
RUN apk update
|
||||
|
||||
# Install common php extensions
|
||||
RUN docker-php-ext-install pdo_mysql
|
||||
RUN docker-php-ext-install calendar
|
||||
|
||||
RUN apk add --no-cache oniguruma-dev
|
||||
RUN docker-php-ext-install mbstring
|
||||
|
||||
RUN apk add --no-cache sqlite-libs
|
||||
RUN apk add --no-cache sqlite-dev
|
||||
RUN docker-php-ext-install pdo_sqlite
|
||||
|
||||
RUN apk add --no-cache icu-dev
|
||||
RUN docker-php-ext-install intl
|
||||
|
||||
RUN apk add --no-cache libzip-dev zlib-dev
|
||||
RUN docker-php-ext-install zip
|
||||
|
||||
RUN apk add --no-cache libpng-dev
|
||||
RUN docker-php-ext-install gd
|
||||
|
||||
RUN apk add --no-cache postgresql-dev
|
||||
RUN docker-php-ext-install pdo_pgsql
|
||||
|
||||
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS linux-headers && \
|
||||
docker-php-ext-install sockets && \
|
||||
apk del .phpize-deps
|
||||
RUN docker-php-ext-install bcmath
|
||||
|
||||
# Install APCu extension
|
||||
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/apcu \
|
||||
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1 \
|
||||
&& docker-php-ext-configure apcu \
|
||||
&& docker-php-ext-install apcu \
|
||||
&& rm /tmp/apcu.tar.gz \
|
||||
&& rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini \
|
||||
&& echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
|
||||
|
||||
# Install inotify extension
|
||||
ADD https://pecl.php.net/get/inotify-$INOTIFY_VERSION.tgz /tmp/inotify.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/inotify \
|
||||
&& tar xf /tmp/inotify.tar.gz -C /usr/src/php/ext/inotify --strip-components=1 \
|
||||
&& docker-php-ext-configure inotify \
|
||||
&& docker-php-ext-install inotify \
|
||||
&& rm /tmp/inotify.tar.gz
|
||||
|
||||
# Install openswoole, pcov and mssql driver
|
||||
RUN wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||
pecl install openswoole-${OPENSWOOLE_VERSION} pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
|
||||
docker-php-ext-enable openswoole pdo_sqlsrv pcov && \
|
||||
apk del .phpize-deps && \
|
||||
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk
|
||||
|
||||
# Install composer
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# Make home directory writable by anyone
|
||||
RUN chmod 777 /home
|
||||
|
||||
VOLUME /home/shlink
|
||||
WORKDIR /home/shlink
|
||||
|
||||
# Expose openswoole port
|
||||
EXPOSE 8080
|
||||
|
||||
CMD \
|
||||
# Install dependencies if the vendor dir does not exist
|
||||
if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \
|
||||
# When restarting the container, openswoole might think it is already in execution
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until php ./vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
|
||||
@@ -1,14 +0,0 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
|
||||
error_log /home/shlink/www/data/infra/nginx/swoole_proxy.error.log;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://shlink_swoole:8080;
|
||||
proxy_read_timeout 90s;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
use function is_subclass_of;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
class Version20160819142757 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @throws Exception
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$platformClass = $this->connection->getDatabasePlatform();
|
||||
$table = $schema->getTable('short_urls');
|
||||
$column = $table->getColumn('short_code');
|
||||
|
||||
match (true) {
|
||||
is_subclass_of($platformClass, MySQLPlatform::class) => $column
|
||||
->setPlatformOption('charset', 'utf8mb4')
|
||||
->setPlatformOption('collation', 'utf8mb4_bin'),
|
||||
is_subclass_of($platformClass, SqlitePlatform::class) => $column->setPlatformOption('collate', 'BINARY'),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// Nothing to roll back
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
class Version20160820191203 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// Check if the tables already exist
|
||||
$tables = $schema->getTables();
|
||||
foreach ($tables as $table) {
|
||||
if ($table->getName() === 'tags') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->createTagsTable($schema);
|
||||
$this->createShortUrlsInTagsTable($schema);
|
||||
}
|
||||
|
||||
private function createTagsTable(Schema $schema): void
|
||||
{
|
||||
$table = $schema->createTable('tags');
|
||||
$table->addColumn('id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addColumn('name', Types::STRING, [
|
||||
'length' => 255,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addUniqueIndex(['name']);
|
||||
|
||||
$table->setPrimaryKey(['id']);
|
||||
}
|
||||
|
||||
private function createShortUrlsInTagsTable(Schema $schema): void
|
||||
{
|
||||
$table = $schema->createTable('short_urls_in_tags');
|
||||
$table->addColumn('short_url_id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addColumn('tag_id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
|
||||
$table->addForeignKeyConstraint('tags', ['tag_id'], ['id'], [
|
||||
'onDelete' => 'CASCADE',
|
||||
'onUpdate' => 'RESTRICT',
|
||||
]);
|
||||
$table->addForeignKeyConstraint('short_urls', ['short_url_id'], ['id'], [
|
||||
'onDelete' => 'CASCADE',
|
||||
'onUpdate' => 'RESTRICT',
|
||||
]);
|
||||
|
||||
$table->setPrimaryKey(['short_url_id', 'tag_id']);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$schema->dropTable('short_urls_in_tags');
|
||||
$schema->dropTable('tags');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
class Version20171021093246 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if ($shortUrls->hasColumn('valid_since')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shortUrls->addColumn('valid_since', Types::DATETIME_MUTABLE, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
$shortUrls->addColumn('valid_until', Types::DATETIME_MUTABLE, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if (! $shortUrls->hasColumn('valid_since')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shortUrls->dropColumn('valid_since');
|
||||
$shortUrls->dropColumn('valid_until');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
class Version20171022064541 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if ($shortUrls->hasColumn('max_visits')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shortUrls->addColumn('max_visits', Types::INTEGER, [
|
||||
'unsigned' => true,
|
||||
'notnull' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if (! $shortUrls->hasColumn('max_visits')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shortUrls->dropColumn('max_visits');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20180801183328 extends AbstractMigration
|
||||
{
|
||||
private const NEW_SIZE = 255;
|
||||
private const OLD_SIZE = 10;
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->setSize($schema, self::NEW_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->setSize($schema, self::OLD_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function setSize(Schema $schema, int $size): void
|
||||
{
|
||||
$schema->getTable('short_urls')->getColumn('short_code')->setLength($size);
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
use PDO;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20180913205455 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// Nothing to create
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function postUp(Schema $schema): void
|
||||
{
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->select('id', 'remote_addr')
|
||||
->from('visits');
|
||||
$st = $this->connection->executeQuery($qb->getSQL());
|
||||
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('visits', 'v')
|
||||
->set('v.remote_addr', ':obfuscatedAddr')
|
||||
->where('v.id=:id');
|
||||
|
||||
while ($row = $st->fetch(PDO::FETCH_ASSOC)) {
|
||||
$addr = $row['remote_addr'] ?? null;
|
||||
if ($addr === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$qb->setParameters([
|
||||
'id' => $row['id'],
|
||||
'obfuscatedAddr' => $this->determineAddress((string) $addr),
|
||||
])->execute();
|
||||
}
|
||||
}
|
||||
|
||||
private function determineAddress(string $addr): ?string
|
||||
{
|
||||
if ($addr === IpAddress::LOCALHOST) {
|
||||
return $addr;
|
||||
}
|
||||
|
||||
try {
|
||||
return (string) IpAddress::fromString($addr)->getAnonymizedCopy();
|
||||
} catch (InvalidArgumentException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// Nothing to rollback
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20180915110857 extends AbstractMigration
|
||||
{
|
||||
private const ON_DELETE_MAP = [
|
||||
'visit_locations' => 'SET NULL',
|
||||
'short_urls' => 'CASCADE',
|
||||
];
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visits = $schema->getTable('visits');
|
||||
$foreignKeys = $visits->getForeignKeys();
|
||||
|
||||
// Remove all existing foreign keys and add them again with CASCADE delete
|
||||
foreach ($foreignKeys as $foreignKey) {
|
||||
$visits->removeForeignKey($foreignKey->getName());
|
||||
$foreignTable = $foreignKey->getForeignTableName();
|
||||
|
||||
$visits->addForeignKeyConstraint(
|
||||
$foreignTable,
|
||||
$foreignKey->getLocalColumns(),
|
||||
$foreignKey->getForeignColumns(),
|
||||
[
|
||||
'onDelete' => self::ON_DELETE_MAP[$foreignTable],
|
||||
'onUpdate' => 'RESTRICT',
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// Nothing to run
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20181020060559 extends AbstractMigration
|
||||
{
|
||||
private const COLUMNS = [
|
||||
'countryCode' => 'country_code',
|
||||
'countryName' => 'country_name',
|
||||
'regionName' => 'region_name',
|
||||
'cityName' => 'city_name',
|
||||
];
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->createColumns($schema->getTable('visit_locations'), self::COLUMNS);
|
||||
}
|
||||
|
||||
private function createColumns(Table $visitLocations, array $columnNames): void
|
||||
{
|
||||
foreach ($columnNames as $name) {
|
||||
if (! $visitLocations->hasColumn($name)) {
|
||||
$visitLocations->addColumn($name, Types::STRING, ['notnull' => false]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function postUp(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
|
||||
// If the camel case columns do not exist, do nothing
|
||||
if (! $visitLocations->hasColumn('countryCode')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('visit_locations');
|
||||
foreach (self::COLUMNS as $camelCaseName => $snakeCaseName) {
|
||||
$qb->set($snakeCaseName, $camelCaseName);
|
||||
}
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// No down
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20181020065148 extends AbstractMigration
|
||||
{
|
||||
private const CAMEL_CASE_COLUMNS = [
|
||||
'countryCode',
|
||||
'countryName',
|
||||
'regionName',
|
||||
'cityName',
|
||||
];
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
|
||||
foreach (self::CAMEL_CASE_COLUMNS as $name) {
|
||||
if ($visitLocations->hasColumn($name)) {
|
||||
$visitLocations->dropColumn($name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// No down
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Column;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20181110175521 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->getUserAgentColumn($schema)->setLength(512);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->getUserAgentColumn($schema)->setLength(256);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function getUserAgentColumn(Schema $schema): Column
|
||||
{
|
||||
return $schema->getTable('visits')->getColumn('user_agent');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Column;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20190824075137 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->getRefererColumn($schema)->setLength(1024);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->getRefererColumn($schema)->setLength(256);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function getRefererColumn(Schema $schema): Column
|
||||
{
|
||||
return $schema->getTable('visits')->getColumn('referer');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20190930165521 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if ($shortUrls->hasColumn('domain_id')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$domains = $schema->createTable('domains');
|
||||
$domains->addColumn('id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$domains->addColumn('authority', Types::STRING, [
|
||||
'length' => 512,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$domains->addUniqueIndex(['authority']);
|
||||
$domains->setPrimaryKey(['id']);
|
||||
|
||||
$shortUrls->addColumn('domain_id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => false,
|
||||
]);
|
||||
$shortUrls->addForeignKeyConstraint('domains', ['domain_id'], ['id'], [
|
||||
'onDelete' => 'RESTRICT',
|
||||
'onUpdate' => 'RESTRICT',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$schema->getTable('short_urls')->dropColumn('domain_id');
|
||||
$schema->dropTable('domains');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Index;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
use function array_reduce;
|
||||
|
||||
final class Version20191001201532 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if ($shortUrls->hasIndex('unique_short_code_plus_domain')) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Index|null $shortCodesIndex */
|
||||
$shortCodesIndex = array_reduce($shortUrls->getIndexes(), function (?Index $found, Index $current) {
|
||||
[$column] = $current->getColumns();
|
||||
return $column === 'short_code' ? $current : $found;
|
||||
});
|
||||
if ($shortCodesIndex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shortUrls->dropIndex($shortCodesIndex->getName());
|
||||
$shortUrls->addUniqueIndex(['short_code', 'domain_id'], 'unique_short_code_plus_domain');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
|
||||
$shortUrls->dropIndex('unique_short_code_plus_domain');
|
||||
$shortUrls->addUniqueIndex(['short_code']);
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Column;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20191020074522 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->getOriginalUrlColumn($schema)->setLength(2048);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->getOriginalUrlColumn($schema)->setLength(1024);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function getOriginalUrlColumn(Schema $schema): Column
|
||||
{
|
||||
return $schema->getTable('short_urls')->getColumn('original_url');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\some;
|
||||
|
||||
final class Version20200105165647 extends AbstractMigration
|
||||
{
|
||||
private const COLUMNS = ['lat' => 'latitude', 'lon' => 'longitude'];
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function preUp(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
$this->skipIf(some(
|
||||
self::COLUMNS,
|
||||
fn (string $v, string|int $newColName) => $visitLocations->hasColumn((string) $newColName),
|
||||
), 'New columns already exist');
|
||||
|
||||
foreach (self::COLUMNS as $columnName) {
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('visit_locations')
|
||||
->set($columnName, ':zeroValue')
|
||||
->where($qb->expr()->orX(
|
||||
$qb->expr()->eq($columnName, ':emptyString'),
|
||||
$qb->expr()->isNull($columnName),
|
||||
))
|
||||
->setParameters([
|
||||
'zeroValue' => '0',
|
||||
'emptyString' => '',
|
||||
])
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
|
||||
foreach (self::COLUMNS as $newName => $oldName) {
|
||||
$visitLocations->addColumn($newName, Types::FLOAT, [
|
||||
'default' => '0.0',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function postUp(Schema $schema): void
|
||||
{
|
||||
$isPostgres = $this->connection->getDatabasePlatform() instanceof PostgreSQLPlatform;
|
||||
$castType = $isPostgres ? 'DOUBLE PRECISION' : 'DECIMAL(9,2)';
|
||||
|
||||
foreach (self::COLUMNS as $newName => $oldName) {
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('visit_locations')
|
||||
->set($newName, 'CAST(' . $oldName . ' AS ' . $castType . ')')
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
public function preDown(Schema $schema): void
|
||||
{
|
||||
foreach (self::COLUMNS as $newName => $oldName) {
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('visit_locations')
|
||||
->set($oldName, $newName)
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
|
||||
foreach (self::COLUMNS as $colName => $oldName) {
|
||||
$visitLocations->dropColumn($colName);
|
||||
}
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20200106215144 extends AbstractMigration
|
||||
{
|
||||
private const COLUMNS = ['latitude', 'longitude'];
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
$this->skipIf($this->oldColumnsDoNotExist($visitLocations), 'Old columns do not exist');
|
||||
|
||||
foreach (self::COLUMNS as $colName) {
|
||||
$visitLocations->dropColumn($colName);
|
||||
}
|
||||
}
|
||||
|
||||
public function oldColumnsDoNotExist(Table $visitLocations): bool
|
||||
{
|
||||
foreach (self::COLUMNS as $oldColName) {
|
||||
if ($visitLocations->hasColumn($oldColName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
|
||||
foreach (self::COLUMNS as $colName) {
|
||||
$visitLocations->addColumn($colName, Types::STRING, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20200110182849 extends AbstractMigration
|
||||
{
|
||||
private const DEFAULT_EMPTY_VALUE = '';
|
||||
private const COLUMN_DEFAULTS_MAP = [
|
||||
'visits' => [
|
||||
'referer',
|
||||
'user_agent',
|
||||
],
|
||||
'visit_locations' => [
|
||||
'timezone',
|
||||
'country_code',
|
||||
'country_name',
|
||||
'region_name',
|
||||
'city_name',
|
||||
],
|
||||
];
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
foreach (self::COLUMN_DEFAULTS_MAP as $tableName => $columns) {
|
||||
foreach ($columns as $columnName) {
|
||||
$this->setDefaultValueForColumnInTable($tableName, $columnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setDefaultValueForColumnInTable(string $tableName, string $columnName): void
|
||||
{
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update($tableName)
|
||||
->set($columnName, ':emptyValue')
|
||||
->setParameter('emptyValue', self::DEFAULT_EMPTY_VALUE)
|
||||
->where($qb->expr()->isNull($columnName))
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// No need (and no way) to undo this migration
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20200323190014 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
$this->skipIf($visitLocations->hasColumn('is_empty'));
|
||||
|
||||
$visitLocations->addColumn('is_empty', Types::BOOLEAN, ['default' => false]);
|
||||
}
|
||||
|
||||
public function postUp(Schema $schema): void
|
||||
{
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('visit_locations')
|
||||
->set('is_empty', ':isEmpty')
|
||||
->where($qb->expr()->eq('country_code', ':emptyString'))
|
||||
->andWhere($qb->expr()->eq('country_name', ':emptyString'))
|
||||
->andWhere($qb->expr()->eq('region_name', ':emptyString'))
|
||||
->andWhere($qb->expr()->eq('city_name', ':emptyString'))
|
||||
->andWhere($qb->expr()->eq('timezone', ':emptyString'))
|
||||
->andWhere($qb->expr()->eq('lat', 0))
|
||||
->andWhere($qb->expr()->eq('lon', 0))
|
||||
->setParameter('isEmpty', true)
|
||||
->setParameter('emptyString', '')
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
$this->skipIf(!$visitLocations->hasColumn('is_empty'));
|
||||
|
||||
$visitLocations->dropColumn('is_empty');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20200503170404 extends AbstractMigration
|
||||
{
|
||||
private const INDEX_NAME = 'IDX_visits_date';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visits = $schema->getTable('visits');
|
||||
$this->skipIf($visits->hasIndex(self::INDEX_NAME));
|
||||
$visits->addIndex(['date'], self::INDEX_NAME);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$visits = $schema->getTable('visits');
|
||||
$this->skipIf(! $visits->hasIndex(self::INDEX_NAME));
|
||||
$visits->dropIndex(self::INDEX_NAME);
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20201023090929 extends AbstractMigration
|
||||
{
|
||||
private const IMPORT_SOURCE_COLUMN = 'import_source';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf($shortUrls->hasColumn(self::IMPORT_SOURCE_COLUMN));
|
||||
|
||||
$shortUrls->addColumn(self::IMPORT_SOURCE_COLUMN, Types::STRING, [
|
||||
'length' => 255,
|
||||
'notnull' => false,
|
||||
]);
|
||||
$shortUrls->addColumn('import_original_short_code', Types::STRING, [
|
||||
'length' => 255,
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
$shortUrls->addUniqueIndex(
|
||||
[self::IMPORT_SOURCE_COLUMN, 'import_original_short_code', 'domain_id'],
|
||||
'unique_imports',
|
||||
);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf(! $shortUrls->hasColumn(self::IMPORT_SOURCE_COLUMN));
|
||||
|
||||
$shortUrls->dropColumn(self::IMPORT_SOURCE_COLUMN);
|
||||
$shortUrls->dropColumn('import_original_short_code');
|
||||
$shortUrls->dropIndex('unique_imports');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\DBAL\Driver\Result;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20201102113208 extends AbstractMigration
|
||||
{
|
||||
private const API_KEY_COLUMN = 'author_api_key_id';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf($shortUrls->hasColumn(self::API_KEY_COLUMN));
|
||||
|
||||
$shortUrls->addColumn(self::API_KEY_COLUMN, Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
$shortUrls->addForeignKeyConstraint('api_keys', [self::API_KEY_COLUMN], ['id'], [
|
||||
'onDelete' => 'SET NULL',
|
||||
'onUpdate' => 'RESTRICT',
|
||||
], 'FK_' . self::API_KEY_COLUMN);
|
||||
}
|
||||
|
||||
public function postUp(Schema $schema): void
|
||||
{
|
||||
// If there's only one API key and it's active, link all existing URLs with it
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->select('id')
|
||||
->from('api_keys')
|
||||
->where($qb->expr()->eq('enabled', ':enabled'))
|
||||
->andWhere($qb->expr()->or(
|
||||
$qb->expr()->isNull('expiration_date'),
|
||||
$qb->expr()->gt('expiration_date', ':expiration'),
|
||||
))
|
||||
->setParameters([
|
||||
'enabled' => true,
|
||||
'expiration' => Chronos::now()->toDateTimeString(),
|
||||
]);
|
||||
|
||||
/** @var Result $result */
|
||||
$result = $qb->execute();
|
||||
$id = $this->resolveOneApiKeyId($result);
|
||||
if ($id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('short_urls')
|
||||
->set(self::API_KEY_COLUMN, ':apiKeyId')
|
||||
->setParameter('apiKeyId', $id)
|
||||
->execute();
|
||||
}
|
||||
|
||||
private function resolveOneApiKeyId(Result $result): string|int|null
|
||||
{
|
||||
$results = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
// As soon as we have to iterate more than once, then we cannot resolve a single API key
|
||||
if (! empty($results)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$results[] = $row['id'] ?? null;
|
||||
}
|
||||
|
||||
return $results[0] ?? null;
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf(! $shortUrls->hasColumn(self::API_KEY_COLUMN));
|
||||
|
||||
$shortUrls->removeForeignKey('FK_' . self::API_KEY_COLUMN);
|
||||
$shortUrls->dropColumn(self::API_KEY_COLUMN);
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20210102174433 extends AbstractMigration
|
||||
{
|
||||
private const TABLE_NAME = 'api_key_roles';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->skipIf($schema->hasTable(self::TABLE_NAME));
|
||||
|
||||
$table = $schema->createTable(self::TABLE_NAME);
|
||||
$table->addColumn('id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->setPrimaryKey(['id']);
|
||||
|
||||
$table->addColumn('role_name', Types::STRING, [
|
||||
'length' => 255,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addColumn('meta', Types::JSON, [
|
||||
'notnull' => true,
|
||||
]);
|
||||
|
||||
$table->addColumn('api_key_id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addForeignKeyConstraint('api_keys', ['api_key_id'], ['id'], [
|
||||
'onDelete' => 'CASCADE',
|
||||
'onUpdate' => 'RESTRICT',
|
||||
]);
|
||||
$table->addUniqueIndex(['role_name', 'api_key_id'], 'UQ_role_plus_api_key');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->skipIf(! $schema->hasTable(self::TABLE_NAME));
|
||||
$schema->getTable(self::TABLE_NAME)->dropIndex('UQ_role_plus_api_key');
|
||||
$schema->dropTable(self::TABLE_NAME);
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20210118153932 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// Prev migration used to set the length to 256, which made some set-ups crash
|
||||
// It has been updated to 255, and this migration ensures whoever managed to run the prev one, gets the value
|
||||
// also updated to 255
|
||||
|
||||
$rolesTable = $schema->getTable('api_key_roles');
|
||||
$nameColumn = $rolesTable->getColumn('role_name');
|
||||
$nameColumn->setLength(255);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20210202181026 extends AbstractMigration
|
||||
{
|
||||
private const TITLE = 'title';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf($shortUrls->hasColumn(self::TITLE));
|
||||
|
||||
$shortUrls->addColumn(self::TITLE, Types::STRING, [
|
||||
'notnull' => false,
|
||||
'length' => 512,
|
||||
]);
|
||||
$shortUrls->addColumn('title_was_auto_resolved', Types::BOOLEAN, [
|
||||
'default' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf(! $shortUrls->hasColumn(self::TITLE));
|
||||
$shortUrls->dropColumn(self::TITLE);
|
||||
$shortUrls->dropColumn('title_was_auto_resolved');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||
|
||||
final class Version20210207100807 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visits = $schema->getTable('visits');
|
||||
$this->skipIf($visits->hasColumn('visited_url'));
|
||||
|
||||
$shortUrlId = $visits->getColumn('short_url_id');
|
||||
$shortUrlId->setNotnull(false);
|
||||
|
||||
$visits->addColumn('visited_url', Types::STRING, [
|
||||
'length' => Visitor::VISITED_URL_MAX_LENGTH,
|
||||
'notnull' => false,
|
||||
]);
|
||||
$visits->addColumn('type', Types::STRING, [
|
||||
'length' => 255,
|
||||
'default' => VisitType::VALID_SHORT_URL->value,
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$visits = $schema->getTable('visits');
|
||||
$this->skipIf(! $visits->hasColumn('visited_url'));
|
||||
|
||||
$shortUrlId = $visits->getColumn('short_url_id');
|
||||
$shortUrlId->setNotnull(true);
|
||||
$visits->dropColumn('visited_url');
|
||||
$visits->dropColumn('type');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20210306165711 extends AbstractMigration
|
||||
{
|
||||
private const TABLE = 'api_keys';
|
||||
private const COLUMN = 'name';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$apiKeys = $schema->getTable(self::TABLE);
|
||||
$this->skipIf($apiKeys->hasColumn(self::COLUMN));
|
||||
|
||||
$apiKeys->addColumn(
|
||||
self::COLUMN,
|
||||
Types::STRING,
|
||||
[
|
||||
'notnull' => false,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$apiKeys = $schema->getTable(self::TABLE);
|
||||
$this->skipIf(! $apiKeys->hasColumn(self::COLUMN));
|
||||
|
||||
$apiKeys->dropColumn(self::COLUMN);
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20210522051601 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf($shortUrls->hasColumn('crawlable'));
|
||||
$shortUrls->addColumn('crawlable', Types::BOOLEAN, ['default' => false]);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf(! $shortUrls->hasColumn('crawlable'));
|
||||
$shortUrls->dropColumn('crawlable');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20210522124633 extends AbstractMigration
|
||||
{
|
||||
private const POTENTIAL_BOT_COLUMN = 'potential_bot';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visits = $schema->getTable('visits');
|
||||
$this->skipIf($visits->hasColumn(self::POTENTIAL_BOT_COLUMN));
|
||||
$visits->addColumn(self::POTENTIAL_BOT_COLUMN, Types::BOOLEAN, ['default' => false]);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$visits = $schema->getTable('visits');
|
||||
$this->skipIf(! $visits->hasColumn(self::POTENTIAL_BOT_COLUMN));
|
||||
$visits->dropColumn(self::POTENTIAL_BOT_COLUMN);
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20210720143824 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$domainsTable = $schema->getTable('domains');
|
||||
$this->skipIf($domainsTable->hasColumn('base_url_redirect'));
|
||||
|
||||
$this->createRedirectColumn($domainsTable, 'base_url_redirect');
|
||||
$this->createRedirectColumn($domainsTable, 'regular_not_found_redirect');
|
||||
$this->createRedirectColumn($domainsTable, 'invalid_short_url_redirect');
|
||||
}
|
||||
|
||||
private function createRedirectColumn(Table $table, string $columnName): void
|
||||
{
|
||||
$table->addColumn($columnName, Types::STRING, [
|
||||
'notnull' => false,
|
||||
'default' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$domainsTable = $schema->getTable('domains');
|
||||
$this->skipIf(! $domainsTable->hasColumn('base_url_redirect'));
|
||||
|
||||
$domainsTable->dropColumn('base_url_redirect');
|
||||
$domainsTable->dropColumn('regular_not_found_redirect');
|
||||
$domainsTable->dropColumn('invalid_short_url_redirect');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211002072605 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf($shortUrls->hasColumn('forward_query'));
|
||||
$shortUrls->addColumn('forward_query', Types::BOOLEAN, ['default' => true]);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf(! $shortUrls->hasColumn('forward_query'));
|
||||
$shortUrls->dropColumn('forward_query');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
shlink_db_mysql:
|
||||
environment:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
shlink_php:
|
||||
user: 1000:1000
|
||||
@@ -7,12 +5,6 @@ services:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_swoole:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_roadrunner:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
shlink_nginx:
|
||||
container_name: shlink_nginx
|
||||
@@ -39,44 +37,6 @@ services:
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
|
||||
shlink_swoole_proxy:
|
||||
container_name: shlink_swoole_proxy
|
||||
image: nginx:1.25-alpine
|
||||
ports:
|
||||
- "8002:80"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
- ./data/infra/swoole_proxy_vhost.conf:/etc/nginx/conf.d/default.conf
|
||||
links:
|
||||
- shlink_swoole
|
||||
|
||||
shlink_swoole:
|
||||
container_name: shlink_swoole
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/swoole.Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- ./:/home/shlink
|
||||
- ./data/infra/php.ini:/usr/local/etc/php/php.ini
|
||||
links:
|
||||
- shlink_db_mysql
|
||||
- shlink_db_postgres
|
||||
- shlink_db_maria
|
||||
- shlink_db_ms
|
||||
- shlink_redis
|
||||
- shlink_redis_acl
|
||||
- shlink_mercure
|
||||
- shlink_mercure_proxy
|
||||
- shlink_rabbitmq
|
||||
- shlink_matomo
|
||||
environment:
|
||||
LC_ALL: C
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
|
||||
shlink_roadrunner:
|
||||
container_name: shlink_roadrunner
|
||||
build:
|
||||
@@ -117,9 +77,9 @@ services:
|
||||
|
||||
shlink_db_postgres:
|
||||
container_name: shlink_db_postgres
|
||||
image: postgres:12.2-alpine
|
||||
image: postgres:16.3-alpine
|
||||
ports:
|
||||
- "5433:5432"
|
||||
- "5434:5432"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
- ./data/infra/database_pg:/var/lib/postgresql/data
|
||||
@@ -143,7 +103,7 @@ services:
|
||||
|
||||
shlink_db_ms:
|
||||
container_name: shlink_db_ms
|
||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||
ports:
|
||||
- "1433:1433"
|
||||
environment:
|
||||
@@ -169,7 +129,7 @@ services:
|
||||
container_name: shlink_mercure_proxy
|
||||
image: nginx:1.25-alpine
|
||||
ports:
|
||||
- "8001:80"
|
||||
- "8002:80"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
- ./data/infra/mercure_proxy_vhost.conf:/etc/nginx/conf.d/default.conf
|
||||
@@ -185,21 +145,21 @@ services:
|
||||
SERVER_NAME: ":80"
|
||||
MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error
|
||||
MERCURE_EXTRA_DIRECTIVES: "cors_origins https://app.shlink.io http://localhost:3000 http://127.0.0.1:3000"
|
||||
MERCURE_EXTRA_DIRECTIVES: "cors_origins https://app.shlink.io http://localhost:3000 http://127.0.0.1:3000 http://localhost:3002 http://127.0.0.1:3002 http://localhost:3005 http://127.0.0.1:3005"
|
||||
|
||||
shlink_rabbitmq:
|
||||
container_name: shlink_rabbitmq
|
||||
image: rabbitmq:3.11-management-alpine
|
||||
ports:
|
||||
- "15672:15672"
|
||||
- "5672:5672"
|
||||
- "15673:15672"
|
||||
- "5673:5672"
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: "rabbit"
|
||||
RABBITMQ_DEFAULT_PASS: "rabbit"
|
||||
|
||||
shlink_swagger_ui:
|
||||
container_name: shlink_swagger_ui
|
||||
image: swaggerapi/swagger-ui:v5.10.3
|
||||
image: swaggerapi/swagger-ui:v5.11.3
|
||||
ports:
|
||||
- "8005:8080"
|
||||
volumes:
|
||||
@@ -207,7 +167,7 @@ services:
|
||||
|
||||
shlink_matomo:
|
||||
container_name: shlink_matomo
|
||||
image: matomo:4.15-apache
|
||||
image: matomo:5.0-apache
|
||||
ports:
|
||||
- "8003:80"
|
||||
volumes:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
This image provides an easy way to set up [shlink](https://shlink.io) on a container-based runtime.
|
||||
|
||||
It exposes a shlink instance served with [RoadRunner](https://roadrunner.dev) or [openswoole](https://openswoole.com/), which can be linked to external databases to persist data.
|
||||
It exposes a shlink instance served with [RoadRunner](https://roadrunner.dev), which can be linked to external databases to persist data.
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
log_errors_max_len=0
|
||||
zend.assertions=1
|
||||
assert.exception=1
|
||||
memory_limit=512M
|
||||
|
||||
@@ -3,6 +3,9 @@ set -e
|
||||
|
||||
cd /etc/shlink
|
||||
|
||||
# Create data directories if they do not exist. This allows data dir to be mounted as an empty dir if needed
|
||||
mkdir -p data/cache data/locks data/log data/proxies
|
||||
|
||||
flags="--no-interaction --clear-db-cache"
|
||||
|
||||
# Skip downloading GeoLite2 db file if the license key env var was not defined or skipping was explicitly set
|
||||
@@ -17,19 +20,6 @@ fi
|
||||
|
||||
php vendor/bin/shlink-installer init ${flags}
|
||||
|
||||
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided and running as root
|
||||
# FIXME: ENABLE_PERIODIC_VISIT_LOCATE is deprecated. Remove cron support in Shlink 4.0.0
|
||||
if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ] && [ "${SHLINK_USER_ID}" = "root" ]; then
|
||||
echo "Configuring periodic visit location..."
|
||||
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
|
||||
/usr/sbin/crond &
|
||||
fi
|
||||
|
||||
if [ "$SHLINK_RUNTIME" = 'openswoole' ]; then
|
||||
# Openswoole is deprecated. Remove in Shlink 4.0.0
|
||||
# When restarting the container, openswoole might think it is already in execution
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
|
||||
elif [ "$SHLINK_RUNTIME" = 'rr' ]; then
|
||||
if [ "$SHLINK_RUNTIME" = 'rr' ]; then
|
||||
./bin/rr serve -c config/roadrunner/.rr.yml
|
||||
fi
|
||||
|
||||
@@ -111,9 +111,6 @@
|
||||
"type": "string",
|
||||
"description": "The original long URL."
|
||||
},
|
||||
"deviceLongUrls": {
|
||||
"$ref": "#/components/schemas/DeviceLongUrls"
|
||||
},
|
||||
"dateCreated": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
@@ -122,11 +119,6 @@
|
||||
"visitsSummary": {
|
||||
"$ref": "#/components/schemas/VisitsSummary"
|
||||
},
|
||||
"visitsCount": {
|
||||
"deprecated": true,
|
||||
"type": "integer",
|
||||
"description": "The number of visits that this short URL has received."
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -155,11 +147,6 @@
|
||||
"shortCode": "12C18",
|
||||
"shortUrl": "https://s.test/12C18",
|
||||
"longUrl": "https://store.steampowered.com",
|
||||
"deviceLongUrls": {
|
||||
"android": "https://store.steampowered.com/android",
|
||||
"ios": "https://store.steampowered.com/ios",
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
"visitsSummary": {
|
||||
"total": 328,
|
||||
@@ -223,24 +210,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"DeviceLongUrls": {
|
||||
"type": "object",
|
||||
"required": ["android", "ios", "desktop"],
|
||||
"properties": {
|
||||
"android": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
|
||||
"type": "string"
|
||||
},
|
||||
"ios": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
|
||||
"type": "string"
|
||||
},
|
||||
"desktop": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Visit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -263,6 +232,11 @@
|
||||
"potentialBot": {
|
||||
"type": "boolean",
|
||||
"description": "Tells if Shlink thinks this visit comes potentially from a bot or crawler"
|
||||
},
|
||||
"visitedUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The originally visited URL that triggered the tracking of this visit"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
@@ -278,7 +252,8 @@
|
||||
"regionName": "California",
|
||||
"timezone": "America/Los_Angeles"
|
||||
},
|
||||
"potentialBot": false
|
||||
"potentialBot": false,
|
||||
"visitedUrl": "https://s.test"
|
||||
}
|
||||
},
|
||||
"OrphanVisit": {
|
||||
@@ -287,11 +262,6 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"visitedUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The originally visited URL that triggered the tracking of this visit"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
1167
docs/changelog-archive/CHANGELOG-1.x.md
Normal file
1167
docs/changelog-archive/CHANGELOG-1.x.md
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user