mirror of
https://github.com/shlinkio/shlink.git
synced 2026-02-28 12:13:13 +08:00
Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a63075eb4c | ||
|
|
97e9dfad67 | ||
|
|
17c4f13568 | ||
|
|
3b5243689b | ||
|
|
4d28adf4a7 | ||
|
|
1b14bb07b1 | ||
|
|
3a43aa4d41 | ||
|
|
2340b4f601 | ||
|
|
664886eddf | ||
|
|
d3570dac0b | ||
|
|
1854cc2f19 | ||
|
|
bff4bd12ae | ||
|
|
549c6605f0 | ||
|
|
f50263d2d9 | ||
|
|
c80ec54508 | ||
|
|
a91a560651 | ||
|
|
a931c60230 | ||
|
|
479a331008 | ||
|
|
5d99b1aef0 | ||
|
|
17e0c9176e | ||
|
|
48d7388bdc | ||
|
|
aa01c034db | ||
|
|
9035161b65 | ||
|
|
df57ca5edb | ||
|
|
0511c73cc8 | ||
|
|
a3554eaf74 | ||
|
|
cb0bac55d2 | ||
|
|
bd5d3f6897 | ||
|
|
5e6e386c5a | ||
|
|
e783bdc456 | ||
|
|
316b88cea6 | ||
|
|
c03eea789c | ||
|
|
bd5d3cb6fa | ||
|
|
e1f2dcc136 | ||
|
|
5e6ebfa5a9 | ||
|
|
a7ed14a1c9 | ||
|
|
f88d57b2b6 | ||
|
|
9dbd15bc0c | ||
|
|
0edb3e5c2c | ||
|
|
7501eca71e | ||
|
|
b145d106b0 | ||
|
|
b4386a3508 | ||
|
|
36e2a9387d | ||
|
|
14c68b4bbe | ||
|
|
d6fedaf926 | ||
|
|
8d35c1dde2 | ||
|
|
85b5f760e5 | ||
|
|
1a4a107952 | ||
|
|
e431395a12 | ||
|
|
cfc3d54122 | ||
|
|
d9d6d5bd9c | ||
|
|
32f465f7a6 | ||
|
|
4cddb573a0 | ||
|
|
2cb8486bb3 | ||
|
|
2a782ab60b | ||
|
|
5bde273d59 | ||
|
|
41e322fd47 | ||
|
|
55885b0f25 | ||
|
|
d419b9d62d | ||
|
|
3bdc05fbc4 | ||
|
|
57053d66a4 | ||
|
|
9d8ea0a4f6 | ||
|
|
46354baae9 | ||
|
|
27c48414da | ||
|
|
25b1138000 | ||
|
|
4cf3bc08f9 | ||
|
|
7e093a3fd8 | ||
|
|
abecf3be02 | ||
|
|
3d9b48c5fd | ||
|
|
ba4a66f772 | ||
|
|
ec839183e8 | ||
|
|
b0ec0601c1 | ||
|
|
637d8334f4 | ||
|
|
6db46b50e9 | ||
|
|
f6b1cc7556 | ||
|
|
65a0a90a51 | ||
|
|
38a7872fbf | ||
|
|
5839cc5926 | ||
|
|
49bd230474 | ||
|
|
074f2135f6 | ||
|
|
ef073d59ca | ||
|
|
a3b2f94339 | ||
|
|
b17c576a30 | ||
|
|
bc4156ca3c | ||
|
|
b747b8448e | ||
|
|
aa4b9fc27e | ||
|
|
3f3c2c3d1e | ||
|
|
4b49f8fb7f | ||
|
|
550f3b28ea | ||
|
|
6d4c232345 | ||
|
|
2d085ad6f4 | ||
|
|
3ea83f5cc3 | ||
|
|
b47bd0fc7a | ||
|
|
27e90c4c26 | ||
|
|
ad1a846d8e | ||
|
|
78f75a06df | ||
|
|
262d714751 | ||
|
|
f71c3bba5c | ||
|
|
8b495064b2 | ||
|
|
57a36204db | ||
|
|
7cc1722858 | ||
|
|
af50887361 | ||
|
|
99c8c6c8d4 | ||
|
|
1d7c9fd553 | ||
|
|
274c454fa4 | ||
|
|
453fcc4675 | ||
|
|
42427bfd74 | ||
|
|
33eedd2270 | ||
|
|
edaf9e34f4 | ||
|
|
965325aa7c | ||
|
|
bdf2bbd0f1 | ||
|
|
dc4aab2cab | ||
|
|
3b1f6c69de | ||
|
|
cdf5082cff | ||
|
|
61686ed6ea | ||
|
|
f63b96fd05 | ||
|
|
228bd83b75 | ||
|
|
a21dcb852a | ||
|
|
6558c37b9a | ||
|
|
e6720cce12 | ||
|
|
22d039c550 | ||
|
|
a21fcd72ce | ||
|
|
058391cf06 | ||
|
|
24e6acc6e8 | ||
|
|
8e3508f28d | ||
|
|
e72b424968 | ||
|
|
56d299a7dc | ||
|
|
575e6bf707 | ||
|
|
e50c21440f | ||
|
|
7cff11080d | ||
|
|
72381f9844 | ||
|
|
7c649e7497 | ||
|
|
eff308cd43 | ||
|
|
bd3745118e | ||
|
|
602ebef02a | ||
|
|
9040937376 | ||
|
|
a11be5b2ff | ||
|
|
6351d0b87d | ||
|
|
fae3434393 | ||
|
|
4013ae87dd | ||
|
|
cb4ba58b08 | ||
|
|
8c94452348 | ||
|
|
ea96a00b12 | ||
|
|
be26dd58c3 | ||
|
|
eaba5edf7f | ||
|
|
12da04ef37 | ||
|
|
8b03532ddb | ||
|
|
112b54ec7d | ||
|
|
ee6a8ede0a | ||
|
|
07ce5f05a2 | ||
|
|
7b04016ca2 |
51
.github/DISCUSSION_TEMPLATE/help-wanted.yml
vendored
Normal file
51
.github/DISCUSSION_TEMPLATE/help-wanted.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
title: 'Help wanted'
|
||||
body:
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Shlink version
|
||||
placeholder: x.y.z
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: PHP version
|
||||
placeholder: x.y.z
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: How do you serve Shlink
|
||||
options:
|
||||
- Self-hosted Apache
|
||||
- Self-hosted nginx
|
||||
- Self-hosted openswoole
|
||||
- Self-hosted RoadRunner
|
||||
- Openswoole Docker image
|
||||
- RoadRunner Docker image
|
||||
- Other (explain in summary)
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Database engine
|
||||
options:
|
||||
- MySQL
|
||||
- MariaDB
|
||||
- PostgreSQL
|
||||
- MicrosoftSQL
|
||||
- SQLite
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Database version
|
||||
placeholder: x.y.z
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Summary
|
||||
value: '<!-- Describe your issue, question or request here. -->'
|
||||
|
||||
38
.github/ISSUE_TEMPLATE/Bug.md
vendored
38
.github/ISSUE_TEMPLATE/Bug.md
vendored
@@ -1,38 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Something on shlink is broken or not working as documented?
|
||||
labels: bug
|
||||
---
|
||||
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
|
||||
-->
|
||||
|
||||
#### How Shlink is set up
|
||||
|
||||
* Shlink Version: x.y.z
|
||||
* PHP Version: x.y.z
|
||||
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Self-hosted RoadRunner|Openswoole Docker image|RoadRunner Docker image
|
||||
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
|
||||
|
||||
#### Summary
|
||||
|
||||
<!-- Provide a summary describing the problem you are experiencing. -->
|
||||
|
||||
#### Current behavior
|
||||
|
||||
<!-- How is it actually behaving (and it shouldn't)? -->
|
||||
|
||||
#### Expected behavior
|
||||
|
||||
<!-- How did you expect it to behave? -->
|
||||
|
||||
#### How to reproduce
|
||||
|
||||
<!-- Provide steps to reproduce the bug. -->
|
||||
64
.github/ISSUE_TEMPLATE/Bug.yml
vendored
Normal file
64
.github/ISSUE_TEMPLATE/Bug.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Bug report
|
||||
description: Something on Shlink is broken or not working as documented?
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Shlink version
|
||||
placeholder: x.y.z
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: PHP version
|
||||
placeholder: x.y.z
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: How do you serve Shlink
|
||||
options:
|
||||
- Self-hosted Apache
|
||||
- Self-hosted nginx
|
||||
- Self-hosted openswoole
|
||||
- Self-hosted RoadRunner
|
||||
- Openswoole Docker image
|
||||
- RoadRunner Docker image
|
||||
- Other (explain in summary)
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Database engine
|
||||
options:
|
||||
- MySQL
|
||||
- MariaDB
|
||||
- PostgreSQL
|
||||
- MicrosoftSQL
|
||||
- SQLite
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Database version
|
||||
placeholder: x.y.z
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Current behavior
|
||||
value: '<!-- How is it actually behaving (and it should not)? -->'
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
value: '<!-- How did you expect it to behave? -->'
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
value: '<!-- Provide steps to reproduce the bug. -->'
|
||||
19
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
19
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Do you find shlink is missing some important feature that would make it more useful?
|
||||
labels: feature
|
||||
---
|
||||
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
|
||||
-->
|
||||
|
||||
#### Summary
|
||||
|
||||
<!-- Describe the new feature you would like to request. -->
|
||||
16
.github/ISSUE_TEMPLATE/Feature_Request.yml
vendored
Normal file
16
.github/ISSUE_TEMPLATE/Feature_Request.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Feature request
|
||||
description: Do you find Shlink is missing some important feature that would make it more useful?
|
||||
labels: ['feature']
|
||||
body:
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Summary
|
||||
value: '<!-- Describe the new feature you would like to request. -->'
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Use case
|
||||
value: '<!-- Explain why do you think this feature would be useful, and what problems would it help to solve. -->'
|
||||
26
.github/ISSUE_TEMPLATE/Question_Support.md
vendored
26
.github/ISSUE_TEMPLATE/Question_Support.md
vendored
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: Question - Support
|
||||
about: Do you have a problem setting up or using shlink?
|
||||
labels: question
|
||||
---
|
||||
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
|
||||
-->
|
||||
|
||||
#### How Shlink is set up
|
||||
|
||||
* Shlink Version: x.y.z
|
||||
* PHP Version: x.y.z
|
||||
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Self-hosted RoadRunner|Openswoole Docker image|RoadRunner Docker image
|
||||
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
|
||||
|
||||
#### Summary
|
||||
|
||||
<!-- Describe the issue you are facing here. -->
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Question - Support
|
||||
about: Do you need help setting up or using Shlink?
|
||||
url: https://github.com/shlinkio/shlink/discussions/new?category=help-wanted
|
||||
2
.github/actions/ci-setup/action.yml
vendored
2
.github/actions/ci-setup/action.yml
vendored
@@ -43,5 +43,5 @@ runs:
|
||||
ini-values: pcov.directory=module
|
||||
- name: Install dependencies
|
||||
if: ${{ inputs.install-deps == 'yes' }}
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
run: composer install --no-interaction --prefer-dist ${{ inputs.php-version == '8.3' && '--ignore-platform-reqs' || '' }}
|
||||
shell: bash
|
||||
|
||||
9
.github/workflows/ci-db-tests.yml
vendored
9
.github/workflows/ci-db-tests.yml
vendored
@@ -13,11 +13,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
php-version: ['8.2', '8.3']
|
||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
||||
env:
|
||||
LC_ALL: C
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install MSSQL ODBC
|
||||
if: ${{ inputs.platform == 'ms' }}
|
||||
run: sudo ./data/infra/ci/install-ms-odbc.sh
|
||||
@@ -27,7 +28,7 @@ jobs:
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0, pdo_sqlsrv-5.10.1
|
||||
php-extensions: openswoole-22.1.0, pdo_sqlsrv-5.11.1
|
||||
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
|
||||
- name: Create test database
|
||||
if: ${{ inputs.platform == 'ms' }}
|
||||
@@ -36,7 +37,7 @@ jobs:
|
||||
run: composer test:db:${{ inputs.platform }}
|
||||
- name: Upload code coverage
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ matrix.php-version == '8.1' && inputs.platform == 'sqlite:ci' }}
|
||||
if: ${{ matrix.php-version == '8.2' && 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
@@ -10,5 +10,5 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- run: docker build -t shlink-docker-image:temp .
|
||||
|
||||
7
.github/workflows/ci-mutation-tests.yml
vendored
7
.github/workflows/ci-mutation-tests.yml
vendored
@@ -13,13 +13,14 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
php-version: ['8.2', '8.3']
|
||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
|
||||
9
.github/workflows/ci-tests.yml
vendored
9
.github/workflows/ci-tests.yml
vendored
@@ -13,9 +13,10 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
php-version: ['8.2', '8.3']
|
||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- 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
|
||||
@@ -25,11 +26,11 @@ jobs:
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
||||
- run: composer test:${{ inputs.test-group }}:ci
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ matrix.php-version == '8.1' }}
|
||||
if: ${{ matrix.php-version == '8.2' }}
|
||||
with:
|
||||
name: coverage-${{ inputs.test-group }}
|
||||
path: |
|
||||
|
||||
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -29,14 +29,14 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
php-version: ['8.2']
|
||||
command: ['cs', 'stan', 'swagger:validate']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
|
||||
- run: composer ${{ matrix.command }}
|
||||
|
||||
@@ -59,17 +59,18 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
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@v3
|
||||
- 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
|
||||
- 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
|
||||
|
||||
@@ -135,10 +136,10 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
php-version: ['8.2']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
|
||||
10
.github/workflows/publish-docker-image.yml
vendored
10
.github/workflows/publish-docker-image.yml
vendored
@@ -2,16 +2,6 @@ name: Build and publish docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'LICENSE'
|
||||
- '.*'
|
||||
- '*.md'
|
||||
- '*.xml'
|
||||
- '*.yml*'
|
||||
- '*.json5'
|
||||
- '*.neon'
|
||||
branches:
|
||||
- develop
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
|
||||
8
.github/workflows/publish-release.yml
vendored
8
.github/workflows/publish-release.yml
vendored
@@ -10,14 +10,14 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
php-version: ['8.2', '8.3']
|
||||
swoole: ['yes', 'no']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||
install-deps: 'no'
|
||||
- if: ${{ matrix.swoole == 'yes' }}
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
needs: ['build']
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: build
|
||||
|
||||
8
.github/workflows/publish-swagger-spec.yml
vendored
8
.github/workflows/publish-swagger-spec.yml
vendored
@@ -10,9 +10,9 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
php-version: ['8.2']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Determine version
|
||||
id: determine_version
|
||||
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
@@ -20,13 +20,13 @@ jobs:
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0
|
||||
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 }}
|
||||
- run: mv docs/swagger/swagger-inlined.json ${{ steps.determine_version.outputs.version }}/open-api-spec.json
|
||||
- name: Publish spec
|
||||
uses: JamesIves/github-pages-deploy-action@4
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
token: ${{ secrets.OAS_PUBLISH_TOKEN }}
|
||||
repository-name: 'shlinkio/shlink-open-api-specs'
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@ vendor/
|
||||
data/database.sqlite
|
||||
data/shlink-tests.db
|
||||
data/GeoLite2-City.*
|
||||
data/infra/matomo
|
||||
docs/swagger-ui*
|
||||
docs/mercure.html
|
||||
docker-compose.override.yml
|
||||
|
||||
145
CHANGELOG.md
145
CHANGELOG.md
@@ -4,6 +4,149 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||
|
||||
## [3.7.1] - 2023-12-17
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* Remove dependency on functional-php library
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1947](https://github.com/shlinkio/shlink/issues/1947) Fix error when importing short URLs while using Postgres.
|
||||
* [#1939](https://github.com/shlinkio/shlink/issues/1939) Fine-tune RoadRunner logs to avoid too many useless info.
|
||||
|
||||
|
||||
## [3.7.0] - 2023-11-25
|
||||
### Added
|
||||
* [#1798](https://github.com/shlinkio/shlink/issues/1798) Experimental support to send visits to an external Matomo instance.
|
||||
* [#1780](https://github.com/shlinkio/shlink/issues/1780) Add new `NO_ORPHAN_VISITS` API key role.
|
||||
|
||||
Keys with this role will always get `0` when fetching orphan visits.
|
||||
|
||||
When trying to delete orphan visits the result will also be `0` and no visits will actually get deleted.
|
||||
|
||||
* [#1879](https://github.com/shlinkio/shlink/issues/1879) Cache namespace can now be customized via config option or `CACHE_NAMESPACE` env var.
|
||||
|
||||
This is important if you are running multiple Shlink instance on the same server, or they share the same Redis instance (even more so if they are on different versions).
|
||||
|
||||
* [#1905](https://github.com/shlinkio/shlink/issues/1905) Add support for PHP 8.3.
|
||||
* [#1927](https://github.com/shlinkio/shlink/issues/1927) Allow redis credentials be URL-decoded before passing them to connection.
|
||||
* [#1834](https://github.com/shlinkio/shlink/issues/1834) Add support for redis encrypted connections using SSL/TLS.
|
||||
|
||||
Encryption should work out of the box if servers schema is set tp `tls` or `rediss`, including support for self-signed certificates.
|
||||
|
||||
This has been tested with AWS ElasticCache using in-transit encryption, and with Digital Ocean Redis database.
|
||||
|
||||
* [#1906](https://github.com/shlinkio/shlink/issues/1906) Add support for RabbitMQ encrypted connections using SSL/TLS.
|
||||
|
||||
In order to enable SLL, you need to pass `RABBITMQ_USE_SSL=true` or the corresponding config option.
|
||||
|
||||
Connections using self-signed certificates should work out of the box.
|
||||
|
||||
This has been tested with AWS RabbitMQ using in-transit encryption, and with CloudAMQP.
|
||||
|
||||
### Changed
|
||||
* [#1799](https://github.com/shlinkio/shlink/issues/1799) RoadRunner/openswoole jobs are not run anymore for tasks that are actually disabled.
|
||||
|
||||
For example, if you did not enable RabbitMQ real-time updates, instead of triggering a job that ends immediately, the job will not even be enqueued.
|
||||
|
||||
* [#1835](https://github.com/shlinkio/shlink/issues/1835) Docker image is now built only when a release is tagged, and new tags are included, for minor and major versions.
|
||||
* [#1055](https://github.com/shlinkio/shlink/issues/1055) Update OAS definition to v3.1.
|
||||
* [#1885](https://github.com/shlinkio/shlink/issues/1885) Update to chronos 3.0.
|
||||
* [#1896](https://github.com/shlinkio/shlink/issues/1896) Requests to health endpoint are no longer logged.
|
||||
* [#1877](https://github.com/shlinkio/shlink/issues/1877) Print a warning when manually running `visit:download-db` command and a GeoLite2 license was not provided.
|
||||
|
||||
### Deprecated
|
||||
* [#1783](https://github.com/shlinkio/shlink/issues/1783) Deprecated support for openswoole. RoadRunner is the best replacement, with the same capabilities, but much easier and convenient to install and manage.
|
||||
|
||||
### Removed
|
||||
* [#1790](https://github.com/shlinkio/shlink/issues/1790) Drop support for PHP 8.1.
|
||||
|
||||
### Fixed
|
||||
* [#1819](https://github.com/shlinkio/shlink/issues/1819) Fix incorrect timeout when running DB commands during Shlink start-up.
|
||||
* [#1901](https://github.com/shlinkio/shlink/issues/1901) Do not allow short URLs with custom slugs containing URL-reserved characters, as they will not work at all afterward.
|
||||
* [#1900](https://github.com/shlinkio/shlink/issues/1900) Fix short URL visits deletion when multi-segment slugs are enabled.
|
||||
|
||||
|
||||
## [3.6.4] - 2023-09-23
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* [#1866](https://github.com/shlinkio/shlink/issues/1866) The `INITIAL_API_KEY` env var is now only relevant for the official docker image.
|
||||
|
||||
Going forward, new non-docker Shlink installations provisioned with env vars that also wish to provide an initial API key, should do it by using the `vendor/bin/shlink-installer init --initial-api-key=%SOME_KEY%` command, instead of using `INITIAL_API_KEY`.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1819](https://github.com/shlinkio/shlink/issues/1819) Fix incorrect timeout when running DB commands during Shlink start-up.
|
||||
* [#1870](https://github.com/shlinkio/shlink/issues/1870) Make sure shared locks include the cache prefix when using Redis.
|
||||
* [#1866](https://github.com/shlinkio/shlink/issues/1866) Fix error when starting docker image with `INITIAL_API_KEY` env var.
|
||||
|
||||
|
||||
## [3.6.3] - 2023-06-14
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1817](https://github.com/shlinkio/shlink/issues/1817) Fix Shlink trying to create SQLite database tables even if they already exist.
|
||||
|
||||
|
||||
## [3.6.2] - 2023-06-08
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1808](https://github.com/shlinkio/shlink/issues/1808) Fix `rr` binary downloading during Shlink update.
|
||||
|
||||
|
||||
## [3.6.1] - 2023-06-04
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1413](https://github.com/shlinkio/shlink/issues/1413) Fix error when creating initial DB in Postgres in a cluster where a default `postgres` db does not exist or the credentials do not grant permissions to connect.
|
||||
* [#1803](https://github.com/shlinkio/shlink/issues/1803) Fix default RoadRunner port when not using docker image.
|
||||
|
||||
|
||||
## [3.6.0] - 2023-05-24
|
||||
### Added
|
||||
* [#1148](https://github.com/shlinkio/shlink/issues/1148) Add support to delete short URL visits.
|
||||
@@ -39,7 +182,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
* Versions with `-openswoole` suffix (like `3.6.0-openswoole`) will always use openswoole as the runtime, even if default one changes in the future.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
* Deprecated `ENABLE_PERIODIC_VISIT_LOCATE` env var. Use an external mechanism to automate visit locations.
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
@@ -6,9 +6,9 @@ You will also see how to ensure the code fulfills the expected code checks, and
|
||||
|
||||
## System dependencies
|
||||
|
||||
The project provides all its dependencies as docker containers through a docker-compose configuration.
|
||||
The project provides all its dependencies as docker containers through a `docker compose` configuration.
|
||||
|
||||
Because of this, the only actual dependencies are [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
|
||||
Because of this, the only actual dependencies are [docker](https://docs.docker.com/get-docker/) and [docker compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
## Setting up the project
|
||||
|
||||
@@ -21,7 +21,7 @@ Then you will have to follow these steps:
|
||||
For example the `common.local.php.dist` file should be copied as `common.local.php`.
|
||||
|
||||
* Copy the file `docker-compose.override.yml.dist` by also removing the `dist` extension.
|
||||
* Start-up the project by running `docker-compose up`.
|
||||
* Start-up the project by running `docker compose up`.
|
||||
|
||||
The first time this command is run, it will create several containers that are used during development, so it may take some time.
|
||||
|
||||
@@ -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 `8000` through nginx+php-fpm and `8080` through openswoole.
|
||||
Once you finish this, you will have the project exposed in ports `8800` through RoadRunner, `8080` through openswoole and `8000` through nginx+php-fpm.
|
||||
|
||||
> Note: The `indocker` shell script is a helper tool used to run commands inside the main docker container.
|
||||
|
||||
@@ -73,12 +73,12 @@ shlink
|
||||
|
||||
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.
|
||||
* `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.
|
||||
* `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 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 or openswoole.
|
||||
|
||||
## Project tests
|
||||
|
||||
@@ -94,7 +94,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 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 or openswoole, 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.
|
||||
|
||||
@@ -125,6 +125,12 @@ Depending on the kind of contribution, maybe not all kinds of tests are needed,
|
||||
* 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
|
||||
|
||||
The project provides a Swagger UI container for dev envs, which can be accessed in http://localhost:8005.
|
||||
|
||||
It will automatically load the contents of `docs/swagger`, so you can make any updates and they will get reflected.
|
||||
|
||||
## Pull request process
|
||||
|
||||
**Important!**: Before starting to work on a pull request, make sure you always [open an issue](https://github.com/shlinkio/shlink/issues/new/choose) first.
|
||||
|
||||
@@ -7,8 +7,8 @@ ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
|
||||
ARG SHLINK_USER_ID='root'
|
||||
ENV SHLINK_USER_ID ${SHLINK_USER_ID}
|
||||
|
||||
ENV OPENSWOOLE_VERSION 22.0.0
|
||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||
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
|
||||
ENV LC_ALL 'C'
|
||||
@@ -29,6 +29,7 @@ RUN \
|
||||
# Install openswoole and 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; \
|
||||
@@ -49,6 +50,7 @@ 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 ; \
|
||||
|
||||
@@ -32,11 +32,11 @@ You can learn how to use the official docker image by reading [the docs](https:/
|
||||
|
||||
The idea is that you can just generate a container using the image and provide the custom config via env vars.
|
||||
|
||||
## Self hosted
|
||||
## Self-hosted
|
||||
|
||||
First, make sure the host where you are going to run shlink fulfills these requirements:
|
||||
|
||||
* PHP 8.1 or 8.2
|
||||
* 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.
|
||||
* xml extension is required if you want to generate QR codes in svg format.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
export APP_ENV=test
|
||||
export TEST_ENV=api
|
||||
export TEST_RUNTIME="${TEST_RUNTIME:-"openswoole"}"
|
||||
export TEST_RUNTIME="${TEST_RUNTIME:-"openswoole"}" # Openswoole is deprecated. Remove in v4.0.0
|
||||
export DB_DRIVER="${DB_DRIVER:-"postgres"}"
|
||||
export GENERATE_COVERAGE="${GENERATE_COVERAGE:-"no"}"
|
||||
|
||||
|
||||
7
build.sh
7
build.sh
@@ -10,6 +10,7 @@ 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"
|
||||
builtContent="./build/${distId}"
|
||||
@@ -30,7 +31,8 @@ cd "${builtContent}"
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies with $composerBin..."
|
||||
composerFlags="--optimize-autoloader --no-progress --no-interaction"
|
||||
# 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
|
||||
|
||||
@@ -38,6 +40,7 @@ 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
|
||||
@@ -46,7 +49,7 @@ fi
|
||||
echo 'Deleting dev files...'
|
||||
rm composer.*
|
||||
|
||||
# Update shlink version in config
|
||||
# Update Shlink version in config
|
||||
sed -i "s/%SHLINK_VERSION%/${version}/g" config/autoload/app_options.global.php
|
||||
|
||||
# Compressing file
|
||||
|
||||
@@ -12,72 +12,75 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"php": "^8.2",
|
||||
"ext-curl": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"akrabat/ip-address-middleware": "^2.1",
|
||||
"cakephp/chronos": "^2.3",
|
||||
"doctrine/migrations": "^3.5",
|
||||
"doctrine/orm": "^2.14",
|
||||
"endroid/qr-code": "^4.7",
|
||||
"geoip2/geoip2": "^2.13",
|
||||
"cakephp/chronos": "^3.0.2",
|
||||
"doctrine/migrations": "^3.6",
|
||||
"doctrine/orm": "^2.16",
|
||||
"endroid/qr-code": "^4.8",
|
||||
"friendsofphp/proxy-manager-lts": "^1.0",
|
||||
"geoip2/geoip2": "^3.0",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"happyr/doctrine-specification": "^2.0",
|
||||
"jaybizzle/crawler-detect": "^1.2.112",
|
||||
"jaybizzle/crawler-detect": "^1.2.116",
|
||||
"laminas/laminas-config": "^3.8",
|
||||
"laminas/laminas-config-aggregator": "^1.13",
|
||||
"laminas/laminas-diactoros": "^2.24",
|
||||
"laminas/laminas-inputfilter": "^2.24",
|
||||
"laminas/laminas-servicemanager": "^3.20",
|
||||
"laminas/laminas-stdlib": "^3.16",
|
||||
"laminas/laminas-diactoros": "^3.3",
|
||||
"laminas/laminas-inputfilter": "^2.27",
|
||||
"laminas/laminas-servicemanager": "^3.21",
|
||||
"laminas/laminas-stdlib": "^3.17",
|
||||
"league/uri": "^6.8",
|
||||
"lstrojny/functional-php": "^1.17",
|
||||
"mezzio/mezzio": "^3.15",
|
||||
"mezzio/mezzio-fastroute": "^3.8",
|
||||
"mezzio/mezzio-problem-details": "^1.11",
|
||||
"mezzio/mezzio-swoole": "^4.6",
|
||||
"matomo/matomo-php-tracker": "^3.2",
|
||||
"mezzio/mezzio": "^3.17",
|
||||
"mezzio/mezzio-fastroute": "^3.10",
|
||||
"mezzio/mezzio-problem-details": "^1.13",
|
||||
"mezzio/mezzio-swoole": "^4.7",
|
||||
"mlocati/ip-lib": "^1.18",
|
||||
"mobiledetect/mobiledetectlib": "^3.74",
|
||||
"ocramius/proxy-manager": "^2.14",
|
||||
"pagerfanta/core": "^3.7",
|
||||
"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.5",
|
||||
"shlinkio/shlink-config": "^2.4",
|
||||
"shlinkio/shlink-event-dispatcher": "^3.0",
|
||||
"shlinkio/shlink-importer": "^5.1",
|
||||
"shlinkio/shlink-installer": "^8.4",
|
||||
"shlinkio/shlink-ip-geolocation": "^3.2",
|
||||
"shlinkio/shlink-json": "^1.0",
|
||||
"spiral/roadrunner": "^2023.1",
|
||||
"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/shlink-json": "^1.1",
|
||||
"spiral/roadrunner": "^2023.2",
|
||||
"spiral/roadrunner-cli": "^2.5",
|
||||
"spiral/roadrunner-http": "^3.0",
|
||||
"spiral/roadrunner-http": "^3.1",
|
||||
"spiral/roadrunner-jobs": "^4.0",
|
||||
"symfony/console": "^6.2",
|
||||
"symfony/filesystem": "^6.2",
|
||||
"symfony/lock": "^6.2",
|
||||
"symfony/process": "^6.2",
|
||||
"symfony/string": "^6.2"
|
||||
"symfony/console": "^6.3",
|
||||
"symfony/filesystem": "^6.3",
|
||||
"symfony/lock": "^6.3",
|
||||
"symfony/process": "^6.3",
|
||||
"symfony/string": "^6.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"cebe/php-openapi": "^1.7",
|
||||
"devizzent/cebe-php-openapi": "^1.0.1",
|
||||
"devster/ubench": "^2.1",
|
||||
"infection/infection": "^0.27",
|
||||
"openswoole/ide-helper": "~22.0.0",
|
||||
"phpstan/phpstan": "^1.9",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpstan/phpstan-doctrine": "^1.3",
|
||||
"phpstan/phpstan-phpunit": "^1.3",
|
||||
"phpstan/phpstan-symfony": "^1.2",
|
||||
"phpunit/php-code-coverage": "^10.0",
|
||||
"phpunit/phpunit": "^10.1",
|
||||
"phpstan/phpstan-symfony": "^1.3",
|
||||
"phpunit/php-code-coverage": "^10.1",
|
||||
"phpunit/phpunit": "^10.4",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~2.3.0",
|
||||
"shlinkio/shlink-test-utils": "^3.6",
|
||||
"symfony/var-dumper": "^6.2",
|
||||
"veewee/composer-run-parallel": "^1.2"
|
||||
"shlinkio/shlink-test-utils": "^3.8",
|
||||
"symfony/var-dumper": "^6.3",
|
||||
"veewee/composer-run-parallel": "^1.3"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/var-exporter": ">=6.3.9,<=6.4.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -87,6 +90,7 @@
|
||||
},
|
||||
"files": [
|
||||
"config/constants.php",
|
||||
"module/Core/functions/array-utils.php",
|
||||
"module/Core/functions/functions.php"
|
||||
]
|
||||
},
|
||||
@@ -137,7 +141,7 @@
|
||||
"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=80 --configuration=infection-api.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": [
|
||||
@@ -148,6 +152,10 @@
|
||||
"@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"
|
||||
|
||||
@@ -11,12 +11,13 @@ return (static function (): array {
|
||||
'redis' => [
|
||||
'servers' => $redisServers,
|
||||
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
|
||||
'decode_credentials' => (bool) EnvVars::REDIS_DECODE_CREDENTIALS->loadFromEnv(false),
|
||||
],
|
||||
];
|
||||
|
||||
return [
|
||||
'cache' => [
|
||||
'namespace' => 'Shlink',
|
||||
'namespace' => EnvVars::CACHE_NAMESPACE->loadFromEnv('Shlink'),
|
||||
...$cacheRedisBlock,
|
||||
],
|
||||
'redis' => $redis,
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
use function Functional\contains;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
|
||||
return (static function (): array {
|
||||
$driver = EnvVars::DB_DRIVER->loadFromEnv();
|
||||
$isMysqlCompatible = contains(['maria', 'mysql'], $driver);
|
||||
$isMysqlCompatible = contains($driver, ['maria', 'mysql']);
|
||||
|
||||
$resolveDriver = static fn () => match ($driver) {
|
||||
'postgres' => 'pdo_pgsql',
|
||||
|
||||
@@ -11,6 +11,7 @@ return [
|
||||
|
||||
'installer' => [
|
||||
'enabled_options' => [
|
||||
Option\Server\RuntimeConfigOption::class,
|
||||
Option\Database\DatabaseDriverConfigOption::class,
|
||||
Option\Database\DatabaseNameConfigOption::class,
|
||||
Option\Database\DatabaseHostConfigOption::class,
|
||||
@@ -28,9 +29,11 @@ return [
|
||||
Option\Visit\VisitsThresholdConfigOption::class,
|
||||
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,
|
||||
@@ -61,10 +64,15 @@ return [
|
||||
Option\QrCode\DefaultRoundBlockSizeConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqEnabledConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqHostConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqUseSslConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqPortConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqUserConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqPasswordConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqVhostConfigOption::class,
|
||||
Option\Matomo\MatomoEnabledConfigOption::class,
|
||||
Option\Matomo\MatomoBaseUrlConfigOption::class,
|
||||
Option\Matomo\MatomoSiteIdConfigOption::class,
|
||||
Option\Matomo\MatomoApiTokenConfigOption::class,
|
||||
],
|
||||
|
||||
'installation_commands' => [
|
||||
@@ -86,6 +94,9 @@ return [
|
||||
InstallationCommand::API_KEY_GENERATE->value => [
|
||||
'command' => 'bin/cli ' . Command\Api\GenerateKeyCommand::NAME,
|
||||
],
|
||||
InstallationCommand::API_KEY_CREATE->value => [
|
||||
'command' => 'bin/cli ' . Command\Api\InitialApiKeyCommand::NAME,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Shlinkio\Shlink\Common\Cache\RedisFactory;
|
||||
use Shlinkio\Shlink\Common\Lock\NamespacedStore;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use Symfony\Component\Lock;
|
||||
@@ -22,11 +23,12 @@ return [
|
||||
Lock\Store\RedisStore::class => ConfigAbstractFactory::class,
|
||||
Lock\LockFactory::class => ConfigAbstractFactory::class,
|
||||
LOCAL_LOCK_FACTORY => ConfigAbstractFactory::class,
|
||||
NamespacedStore::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'lock_store' => EnvVars::REDIS_SERVERS->existsInEnv() ? 'redis_lock_store' : 'local_lock_store',
|
||||
|
||||
'redis_lock_store' => Lock\Store\RedisStore::class,
|
||||
'redis_lock_store' => NamespacedStore::class,
|
||||
'local_lock_store' => Lock\Store\FlockStore::class,
|
||||
],
|
||||
'delegators' => [
|
||||
@@ -39,6 +41,8 @@ return [
|
||||
ConfigAbstractFactory::class => [
|
||||
Lock\Store\FlockStore::class => ['config.locks.locks_dir'],
|
||||
Lock\Store\RedisStore::class => [RedisFactory::SERVICE_NAME],
|
||||
NamespacedStore::class => [Lock\Store\RedisStore::class, 'config.cache.namespace'],
|
||||
|
||||
Lock\LockFactory::class => ['lock_store'],
|
||||
LOCAL_LOCK_FACTORY => ['local_lock_store'],
|
||||
],
|
||||
|
||||
@@ -52,6 +52,7 @@ return (static function (): array {
|
||||
],
|
||||
],
|
||||
|
||||
// Deprecated. Remove in Shlink 4.0.0
|
||||
'mezzio-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'logger' => [
|
||||
|
||||
16
config/autoload/matomo.global.php
Normal file
16
config/autoload/matomo.global.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return [
|
||||
|
||||
'matomo' => [
|
||||
'enabled' => (bool) EnvVars::MATOMO_ENABLED->loadFromEnv(false),
|
||||
'base_url' => EnvVars::MATOMO_BASE_URL->loadFromEnv(),
|
||||
'site_id' => EnvVars::MATOMO_SITE_ID->loadFromEnv(),
|
||||
'api_token' => EnvVars::MATOMO_API_TOKEN->loadFromEnv(),
|
||||
],
|
||||
|
||||
];
|
||||
26
config/autoload/matomo.local.php.dist
Normal file
26
config/autoload/matomo.local.php.dist
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Dev matomo instance needs to be manually configured once before enabling the configuration below.
|
||||
*
|
||||
* 1. Go to http://localhost:8003 and follow the installation instructions.
|
||||
* 2. Open data/infra/matomo/config/config.ini.php and replace `trusted_hosts[] = "localhost"` with
|
||||
* `trusted_hosts[] = "localhost:8003"` (see https://github.com/matomo-org/matomo/issues/9549)
|
||||
* 3. Go to http://localhost:8003/index.php?module=SitesManager&action=index and paste the ID for the site you just
|
||||
* created into the `site_id` field below.
|
||||
* 4. Go to http://localhost:8003/index.php?module=UsersManager&action=userSecurity, scroll down, click
|
||||
* "Create new token" and once generated, paste the token into the `api_token` field below.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
'matomo' => [
|
||||
// 'enabled' => true,
|
||||
// 'base_url' => 'http://shlink_matomo',
|
||||
// 'site_id' => '...',
|
||||
// 'api_token' => '...',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
'rabbitmq' => [
|
||||
'enabled' => (bool) EnvVars::RABBITMQ_ENABLED->loadFromEnv(false),
|
||||
'host' => EnvVars::RABBITMQ_HOST->loadFromEnv(),
|
||||
'use_ssl' => (bool) EnvVars::RABBITMQ_USE_SSL->loadFromEnv(false),
|
||||
'port' => (int) EnvVars::RABBITMQ_PORT->loadFromEnv('5672'),
|
||||
'user' => EnvVars::RABBITMQ_USER->loadFromEnv(),
|
||||
'password' => EnvVars::RABBITMQ_PASSWORD->loadFromEnv(),
|
||||
|
||||
@@ -32,8 +32,11 @@ return (static function (): array {
|
||||
...ConfigProvider::applyRoutesPrefix([
|
||||
Action\HealthAction::getRouteDef(),
|
||||
|
||||
// Visits
|
||||
// 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(),
|
||||
Action\Visit\DomainVisitsAction::getRouteDef(),
|
||||
Action\Visit\GlobalVisitsAction::getRouteDef(),
|
||||
@@ -54,7 +57,6 @@ return (static function (): array {
|
||||
]),
|
||||
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ResolveShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ListShortUrlsAction::getRouteDef(),
|
||||
|
||||
|
||||
@@ -2,11 +2,26 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Tools\Console\ConsoleRunner;
|
||||
use Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager;
|
||||
use Doctrine\Migrations\Configuration\Migration\ConfigurationArray;
|
||||
use Doctrine\Migrations\DependencyFactory;
|
||||
|
||||
// This file is currently used by doctrine migrations only
|
||||
|
||||
return (static function () {
|
||||
/** @var EntityManager $em */
|
||||
$migrationsConfig = [
|
||||
'migrations_paths' => [
|
||||
'ShlinkMigrations' => 'data/migrations',
|
||||
],
|
||||
'table_storage' => [
|
||||
'table_name' => 'migrations',
|
||||
],
|
||||
'custom_template' => 'data/migrations_template.txt',
|
||||
];
|
||||
$em = include __DIR__ . '/entity-manager.php';
|
||||
return ConsoleRunner::createHelperSet($em);
|
||||
|
||||
return DependencyFactory::fromEntityManager(
|
||||
new ConfigurationArray($migrationsConfig),
|
||||
new ExistingEntityManager($em),
|
||||
);
|
||||
})();
|
||||
|
||||
@@ -22,34 +22,39 @@ use const PHP_SAPI;
|
||||
$isTestEnv = env('APP_ENV') === 'test';
|
||||
$enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoadRunner();
|
||||
|
||||
return (new ConfigAggregator\ConfigAggregator([
|
||||
! $isTestEnv
|
||||
? new EnvVarLoaderProvider('config/params/generated_config.php', enumValues(Core\Config\EnvVars::class))
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
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,
|
||||
Config\ConfigProvider::class,
|
||||
Importer\ConfigProvider::class,
|
||||
IpGeolocation\ConfigProvider::class,
|
||||
EventDispatcher\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
CLI\ConfigProvider::class,
|
||||
Rest\ConfigProvider::class,
|
||||
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
|
||||
$isTestEnv
|
||||
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
// Routes have to be loaded last
|
||||
new ConfigAggregator\PhpFileProvider('config/autoload/routes.config.php'),
|
||||
], 'data/cache/app_config.php', [
|
||||
Core\Config\PostProcessor\BasePathPrefixer::class,
|
||||
Core\Config\PostProcessor\MultiSegmentSlugProcessor::class,
|
||||
Core\Config\PostProcessor\ShortUrlMethodsProcessor::class,
|
||||
]))->getMergedConfig();
|
||||
return (new ConfigAggregator\ConfigAggregator(
|
||||
providers: [
|
||||
! $isTestEnv
|
||||
? new EnvVarLoaderProvider('config/params/generated_config.php', enumValues(Core\Config\EnvVars::class))
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
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,
|
||||
Config\ConfigProvider::class,
|
||||
Importer\ConfigProvider::class,
|
||||
IpGeolocation\ConfigProvider::class,
|
||||
EventDispatcher\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
CLI\ConfigProvider::class,
|
||||
Rest\ConfigProvider::class,
|
||||
new ConfigAggregator\PhpFileProvider('config/autoload/{,*.}global.php'),
|
||||
// Local config should not be loaded during tests, whereas test config should be loaded ONLY during tests
|
||||
new ConfigAggregator\PhpFileProvider(
|
||||
$isTestEnv ? 'config/test/*.global.php' : 'config/autoload/{,*.}local.php',
|
||||
),
|
||||
// Routes have to be loaded last
|
||||
new ConfigAggregator\PhpFileProvider('config/autoload/routes.config.php'),
|
||||
],
|
||||
cachedConfigFile: 'data/cache/app_config.php',
|
||||
postProcessors: [
|
||||
Core\Config\PostProcessor\BasePathPrefixer::class,
|
||||
Core\Config\PostProcessor\MultiSegmentSlugProcessor::class,
|
||||
Core\Config\PostProcessor\ShortUrlMethodsProcessor::class,
|
||||
],
|
||||
))->getMergedConfig();
|
||||
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version: '3.0'
|
||||
version: '3'
|
||||
|
||||
rpc:
|
||||
listen: tcp://127.0.0.1:6001
|
||||
@@ -38,3 +38,5 @@ logs:
|
||||
level: debug
|
||||
metrics:
|
||||
level: debug
|
||||
jobs:
|
||||
level: debug
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version: '3.0'
|
||||
version: '3'
|
||||
|
||||
rpc:
|
||||
listen: tcp://127.0.0.1:6001
|
||||
@@ -7,18 +7,18 @@ server:
|
||||
command: 'php -dopcache.enable_cli=1 -dopcache.validate_timestamps=0 ../../bin/roadrunner-worker.php'
|
||||
|
||||
http:
|
||||
address: '0.0.0.0:${PORT}'
|
||||
address: '0.0.0.0:${PORT:-8080}'
|
||||
middleware: ['static']
|
||||
static:
|
||||
dir: '../../public'
|
||||
forbid: ['.php', '.htaccess']
|
||||
pool:
|
||||
num_workers: ${WEB_WORKER_NUM}
|
||||
num_workers: ${WEB_WORKER_NUM:-0}
|
||||
|
||||
jobs:
|
||||
timeout: 300 # 5 minutes
|
||||
pool:
|
||||
num_workers: ${TASK_WORKER_NUM}
|
||||
num_workers: ${TASK_WORKER_NUM:-0}
|
||||
consume: ['shlink']
|
||||
pipelines:
|
||||
shlink:
|
||||
@@ -33,4 +33,6 @@ logs:
|
||||
http:
|
||||
mode: 'off' # Disable logging as Shlink handles it internally
|
||||
server:
|
||||
level: debug # Everything written to worker stderr is logged
|
||||
level: info
|
||||
jobs:
|
||||
level: debug
|
||||
|
||||
@@ -28,9 +28,9 @@ use Symfony\Component\Console\Event\ConsoleTerminateEvent;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
use function file_exists;
|
||||
use function Functional\contains;
|
||||
use function Laminas\Stratigility\middleware;
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
use function sprintf;
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
@@ -41,7 +41,7 @@ $isApiTest = env('TEST_ENV') === 'api';
|
||||
$isCliTest = env('TEST_ENV') === 'cli';
|
||||
$isE2eTest = $isApiTest || $isCliTest;
|
||||
$coverageType = env('GENERATE_COVERAGE');
|
||||
$generateCoverage = contains(['yes', 'pretty'], $coverageType);
|
||||
$generateCoverage = contains($coverageType, ['yes', 'pretty']);
|
||||
|
||||
$coverage = null;
|
||||
if ($isE2eTest && $generateCoverage) {
|
||||
|
||||
@@ -11,7 +11,7 @@ server {
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ FROM php:8.2-fpm-alpine3.17
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.21
|
||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||
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
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ FROM php:8.2-alpine3.17
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.21
|
||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||
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
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.21
|
||||
ENV INOTIFY_VERSION 3.0.0
|
||||
ENV OPENSWOOLE_VERSION 22.0.0
|
||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||
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
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
use function Functional\some;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\some;
|
||||
|
||||
final class Version20200105165647 extends AbstractMigration
|
||||
{
|
||||
@@ -25,7 +25,7 @@ final class Version20200105165647 extends AbstractMigration
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
$this->skipIf(some(
|
||||
self::COLUMNS,
|
||||
fn (string $v, string $newColName) => $visitLocations->hasColumn($newColName),
|
||||
fn (string $v, string|int $newColName) => $visitLocations->hasColumn((string) $newColName),
|
||||
), 'New columns already exist');
|
||||
|
||||
foreach (self::COLUMNS as $columnName) {
|
||||
|
||||
@@ -7,11 +7,10 @@ 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;
|
||||
|
||||
use function Functional\none;
|
||||
|
||||
final class Version20200106215144 extends AbstractMigration
|
||||
{
|
||||
private const COLUMNS = ['latitude', 'longitude'];
|
||||
@@ -22,16 +21,24 @@ final class Version20200106215144 extends AbstractMigration
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
$this->skipIf(none(
|
||||
self::COLUMNS,
|
||||
fn (string $oldColName) => $visitLocations->hasColumn($oldColName),
|
||||
), 'Old columns do not exist');
|
||||
$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
|
||||
*/
|
||||
|
||||
@@ -9,9 +9,6 @@ use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
use function Functional\each;
|
||||
use function Functional\partial_left;
|
||||
|
||||
final class Version20200110182849 extends AbstractMigration
|
||||
{
|
||||
private const DEFAULT_EMPTY_VALUE = '';
|
||||
@@ -31,11 +28,11 @@ final class Version20200110182849 extends AbstractMigration
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
each(
|
||||
self::COLUMN_DEFAULTS_MAP,
|
||||
fn (array $columns, string $tableName) =>
|
||||
each($columns, partial_left([$this, 'setDefaultValueForColumnInTable'], $tableName)),
|
||||
);
|
||||
foreach (self::COLUMN_DEFAULTS_MAP as $tableName => $columns) {
|
||||
foreach ($columns as $columnName) {
|
||||
$this->setDefaultValueForColumnInTable($tableName, $columnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@ version: '3'
|
||||
services:
|
||||
shlink_nginx:
|
||||
container_name: shlink_nginx
|
||||
image: nginx:1.19.6-alpine
|
||||
image: nginx:1.25-alpine
|
||||
ports:
|
||||
- "8000:80"
|
||||
volumes:
|
||||
@@ -33,6 +33,7 @@ services:
|
||||
- shlink_mercure
|
||||
- shlink_mercure_proxy
|
||||
- shlink_rabbitmq
|
||||
- shlink_matomo
|
||||
environment:
|
||||
LC_ALL: C
|
||||
extra_hosts:
|
||||
@@ -40,7 +41,7 @@ services:
|
||||
|
||||
shlink_swoole_proxy:
|
||||
container_name: shlink_swoole_proxy
|
||||
image: nginx:1.19.6-alpine
|
||||
image: nginx:1.25-alpine
|
||||
ports:
|
||||
- "8002:80"
|
||||
volumes:
|
||||
@@ -70,6 +71,7 @@ services:
|
||||
- shlink_mercure
|
||||
- shlink_mercure_proxy
|
||||
- shlink_rabbitmq
|
||||
- shlink_matomo
|
||||
environment:
|
||||
LC_ALL: C
|
||||
extra_hosts:
|
||||
@@ -95,6 +97,7 @@ services:
|
||||
- shlink_mercure
|
||||
- shlink_mercure_proxy
|
||||
- shlink_rabbitmq
|
||||
- shlink_matomo
|
||||
environment:
|
||||
LC_ALL: C
|
||||
extra_hosts:
|
||||
@@ -164,7 +167,7 @@ services:
|
||||
|
||||
shlink_mercure_proxy:
|
||||
container_name: shlink_mercure_proxy
|
||||
image: nginx:1.19.6-alpine
|
||||
image: nginx:1.25-alpine
|
||||
ports:
|
||||
- "8001:80"
|
||||
volumes:
|
||||
@@ -175,7 +178,7 @@ services:
|
||||
|
||||
shlink_mercure:
|
||||
container_name: shlink_mercure
|
||||
image: dunglas/mercure:v0.14
|
||||
image: dunglas/mercure:v0.15
|
||||
ports:
|
||||
- "3080:80"
|
||||
environment:
|
||||
@@ -186,10 +189,36 @@ services:
|
||||
|
||||
shlink_rabbitmq:
|
||||
container_name: shlink_rabbitmq
|
||||
image: rabbitmq:3.9-management-alpine
|
||||
image: rabbitmq:3.11-management-alpine
|
||||
ports:
|
||||
- "15672:15672"
|
||||
- "5672: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
|
||||
ports:
|
||||
- "8005:8080"
|
||||
volumes:
|
||||
- ./docs/swagger:/app
|
||||
|
||||
shlink_matomo:
|
||||
container_name: shlink_matomo
|
||||
image: matomo:4.15-apache
|
||||
ports:
|
||||
- "8003:80"
|
||||
volumes:
|
||||
# Matomo does not persist port in trusted hosts. This volume is needed to edit config afterward
|
||||
# https://github.com/matomo-org/matomo/issues/9549
|
||||
- ./data/infra/matomo:/var/www/html
|
||||
links:
|
||||
- shlink_db_mysql
|
||||
environment:
|
||||
MATOMO_DATABASE_HOST: "shlink_db_mysql"
|
||||
MATOMO_DATABASE_ADAPTER: "mysql"
|
||||
MATOMO_DATABASE_DBNAME: "matomo"
|
||||
MATOMO_DATABASE_USERNAME: "root"
|
||||
MATOMO_DATABASE_PASSWORD: "root"
|
||||
|
||||
@@ -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 [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) or [openswoole](https://openswoole.com/), which can be linked to external databases to persist data.
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -3,35 +3,33 @@ set -e
|
||||
|
||||
cd /etc/shlink
|
||||
|
||||
flags="--clear-db-cache"
|
||||
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
|
||||
if [ -z "${GEOLITE_LICENSE_KEY}" ] || [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" == "true" ]; then
|
||||
if [ -z "${GEOLITE_LICENSE_KEY}" ] || [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" = "true" ]; then
|
||||
flags="${flags} --skip-download-geolite"
|
||||
fi
|
||||
|
||||
# If INITIAL_API_KEY was provided, create an initial API key
|
||||
if [ -n "${INITIAL_API_KEY}" ]; then
|
||||
flags="${flags} --initial-api-key=${INITIAL_API_KEY}"
|
||||
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
|
||||
# ENABLE_PERIODIC_VISIT_LOCATE is deprecated. Remove cron support in Shlink 4.0.0
|
||||
# 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
|
||||
|
||||
# RoadRunner config needs these to have been set, so falling back to default values if not set yet
|
||||
if [ "$SHLINK_RUNTIME" == 'rr' ]; then
|
||||
export PORT="${PORT:-"8080"}"
|
||||
# Default to 0 so that RoadRunner decides the number of workers based on the amount of logical CPUs
|
||||
export WEB_WORKER_NUM="${WEB_WORKER_NUM:-"0"}"
|
||||
export TASK_WORKER_NUM="${TASK_WORKER_NUM:-"0"}"
|
||||
fi
|
||||
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then
|
||||
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
|
||||
elif [ "$SHLINK_RUNTIME" = 'rr' ]; then
|
||||
./bin/rr serve -c config/roadrunner/.rr.yml
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Build `latest` docker image only for actual releases
|
||||
|
||||
* Status: Accepted
|
||||
* Date: 2023-07-09
|
||||
|
||||
## Context and problem statement
|
||||
|
||||
Historically, this project has re-tagged the `latest´ docker image every time a PR was merged into default branch.
|
||||
|
||||
The reason was to be able to:
|
||||
|
||||
* Periodically test the docker building and publishing process.
|
||||
* Provide "partial" images for quick testing of new "un-released" features.
|
||||
|
||||
However, this was considered non-stable, and not recommended to use in production. Instead, a convenient `stable` tag was provided, which was re-tagged for every new non-beta/non-alpha release.
|
||||
|
||||
The approach described above for `latest` has some problems, though:
|
||||
|
||||
* Many people ignore the recommendation of not using it in production. There have even been reports of bugs on things which were, technically speaking, not yet released.
|
||||
* Since it is not always built for an actual new project version, the project itself cannot inform about anything other than `latest`, which can quickly become a lie if you don't update your local version.
|
||||
|
||||
## Considered options
|
||||
|
||||
* Try to provide a pseudo-version when `latest` is built. Something like `<prev_version>-<commit_hash>.
|
||||
* Change how `latest` is published, and start tagging it only for actual new version releases.
|
||||
* Same as the above, but exclude alpha/beta versions, deprecating `stable` tag.
|
||||
|
||||
## Decision outcome
|
||||
|
||||
Since testing un-released features has never been needed, it is probably a not-very useful thing to have.
|
||||
|
||||
Periodically testing the build and publish process can also be moved somewhere else, like a testing "hidden" account.
|
||||
|
||||
Also, having `stable` with non-alpha/non-beta releases seems sensible, so the decision is to "Change how `latest` is published, and start tagging it only for actual new version releases".
|
||||
|
||||
## Pros and Cons of the Options
|
||||
|
||||
### Try to provide a pseudo-version when `latest` is built.
|
||||
|
||||
* Good: because we keep publishing process intact, from a user point of view.
|
||||
* Bad: because it requires adding some non-trivial logic to the image building, which needs to find out what was the latest stable release.
|
||||
|
||||
### Make `latest` hold latest published version, including unstable releases.
|
||||
|
||||
* Good: because it provides a way for users to test bleeding-edge features, with less risk than relying on the very last content from default branch.
|
||||
* Good: because it allows for `stable` to be used together with `latest`.
|
||||
* Bad: because partial features cannot be tested without publishing an alpha or beta version.
|
||||
|
||||
### Make `latest` hold latest published version, excluding unstable releases.
|
||||
|
||||
* Bad: because there's no longer a way to test bleeding-edge features, other than installing that specific version.
|
||||
* Bad: because it drives `stable` useless, which means it needs to be deprecated, documented, and eventually removed.
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
Here listed you will find the different architectural decisions taken in the project, including all the reasoning behind it, options considered, and final outcome.
|
||||
|
||||
* [2023-07-09Build `latest` docker image only for actual releases](2023-07-09-build-latest-docker-image-only-for-actual-releases.md)
|
||||
* [2023-01-06 Support any HTTP method in short URLs](2023-01-06-support-any-http-method-in-short-urls.md)
|
||||
* [2022-08-05 Support multi-segment custom slugs](2022-08-05-support-multi-segment-custom-slugs.md)
|
||||
* [2022-01-15 Update env vars behavior to have precedence over installer options](2022-01-15-update-env-vars-behavior-to-have-precedence-over-installer-options.md)
|
||||
|
||||
@@ -3,18 +3,15 @@
|
||||
"properties": {
|
||||
"android": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
|
||||
"type": "string",
|
||||
"nullable": false
|
||||
"type": ["string"]
|
||||
},
|
||||
"ios": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
|
||||
"type": "string",
|
||||
"nullable": false
|
||||
"type": ["string"]
|
||||
},
|
||||
"desktop": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
|
||||
"type": "string",
|
||||
"nullable": false
|
||||
"type": ["string"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
}],
|
||||
"properties": {
|
||||
"android": {
|
||||
"nullable": true
|
||||
"type": ["null"]
|
||||
},
|
||||
"ios": {
|
||||
"nullable": true
|
||||
"type": ["null"]
|
||||
},
|
||||
"desktop": {
|
||||
"nullable": true
|
||||
"type": ["null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"baseUrlRedirect": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"type": ["string", "null"],
|
||||
"description": "URL to redirect to when a user hits the domain's base URL"
|
||||
},
|
||||
"regular404Redirect": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"type": ["string", "null"],
|
||||
"description": "URL to redirect to when a user hits a not found URL other than an invalid short URL"
|
||||
},
|
||||
"invalidShortUrlRedirect": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"type": ["string", "null"],
|
||||
"description": "URL to redirect to when a user hits an invalid short URL"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
}],
|
||||
"properties": {
|
||||
"visitedUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"type": ["string", "null"],
|
||||
"description": "The originally visited URL that triggered the tracking of this visit"
|
||||
},
|
||||
"type": {
|
||||
|
||||
@@ -55,13 +55,11 @@
|
||||
"$ref": "./ShortUrlMeta.json"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"type": ["string", "null"],
|
||||
"description": "The domain in which the short URL was created. Null if it belongs to default domain."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"type": ["string", "null"],
|
||||
"description": "A descriptive title of the short URL."
|
||||
},
|
||||
"crawlable": {
|
||||
|
||||
@@ -10,18 +10,15 @@
|
||||
},
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"validUntil": {
|
||||
"description": "The date (in ISO-8601 format) until which this short code will be valid",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"maxVisits": {
|
||||
"description": "The maximum number of allowed visits for this short code",
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
"type": ["number", "null"]
|
||||
},
|
||||
"validateUrl": {
|
||||
"deprecated": true,
|
||||
@@ -36,9 +33,8 @@
|
||||
"description": "The list of tags to set to the short URL."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "A descriptive title of the short URL.",
|
||||
"nullable": true
|
||||
"type": ["string", "null"],
|
||||
"description": "A descriptive title of the short URL."
|
||||
},
|
||||
"crawlable": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -4,18 +4,15 @@
|
||||
"properties": {
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"validUntil": {
|
||||
"description": "The date (in ISO-8601 format) until which this short code will be valid",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"maxVisits": {
|
||||
"description": "The maximum number of allowed visits for this short code",
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
"type": ["number", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Shlink",
|
||||
"description": "Shlink, the self-hosted URL shortener",
|
||||
|
||||
2
indocker
2
indocker
@@ -2,7 +2,7 @@
|
||||
|
||||
# Run docker containers if they are not up yet
|
||||
if ! [[ $(docker ps | grep shlink_swoole) ]]; then
|
||||
docker-compose up -d
|
||||
docker compose up -d
|
||||
fi
|
||||
|
||||
docker exec -it shlink_swoole /bin/sh -c "$*"
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'migrations_paths' => [
|
||||
'ShlinkMigrations' => 'data/migrations',
|
||||
],
|
||||
'table_storage' => [
|
||||
'table_name' => 'migrations',
|
||||
],
|
||||
'custom_template' => 'data/migrations_template.txt',
|
||||
|
||||
];
|
||||
@@ -24,6 +24,7 @@ return [
|
||||
Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
|
||||
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,
|
||||
Command\Api\ListKeysCommand::NAME => Command\Api\ListKeysCommand::class,
|
||||
Command\Api\InitialApiKeyCommand::NAME => Command\Api\InitialApiKeyCommand::class,
|
||||
|
||||
Command\Tag\ListTagsCommand::NAME => Command\Tag\ListTagsCommand::class,
|
||||
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
||||
|
||||
@@ -53,6 +53,7 @@ return [
|
||||
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\ListKeysCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\InitialApiKeyCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Tag\ListTagsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class,
|
||||
@@ -105,6 +106,7 @@ return [
|
||||
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class],
|
||||
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
|
||||
Command\Api\ListKeysCommand::class => [ApiKeyService::class],
|
||||
Command\Api\InitialApiKeyCommand::class => [ApiKeyService::class],
|
||||
|
||||
Command\Tag\ListTagsCommand::class => [TagService::class],
|
||||
Command\Tag\RenameTagCommand::class => [TagService::class],
|
||||
|
||||
@@ -14,24 +14,27 @@ use function is_string;
|
||||
|
||||
class RoleResolver implements RoleResolverInterface
|
||||
{
|
||||
public function __construct(private DomainServiceInterface $domainService, private string $defaultDomain)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DomainServiceInterface $domainService,
|
||||
private readonly string $defaultDomain,
|
||||
) {
|
||||
}
|
||||
|
||||
public function determineRoles(InputInterface $input): array
|
||||
public function determineRoles(InputInterface $input): iterable
|
||||
{
|
||||
$domainAuthority = $input->getOption(Role::DOMAIN_SPECIFIC->paramName());
|
||||
$author = $input->getOption(Role::AUTHORED_SHORT_URLS->paramName());
|
||||
$noOrphanVisits = $input->getOption(Role::NO_ORPHAN_VISITS->paramName());
|
||||
|
||||
$roleDefinitions = [];
|
||||
if ($author) {
|
||||
$roleDefinitions[] = RoleDefinition::forAuthoredShortUrls();
|
||||
yield RoleDefinition::forAuthoredShortUrls();
|
||||
}
|
||||
if (is_string($domainAuthority)) {
|
||||
$roleDefinitions[] = $this->resolveRoleForAuthority($domainAuthority);
|
||||
yield $this->resolveRoleForAuthority($domainAuthority);
|
||||
}
|
||||
if ($noOrphanVisits) {
|
||||
yield RoleDefinition::forNoOrphanVisits();
|
||||
}
|
||||
|
||||
return $roleDefinitions;
|
||||
}
|
||||
|
||||
private function resolveRoleForAuthority(string $domainAuthority): RoleDefinition
|
||||
|
||||
@@ -10,7 +10,7 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
interface RoleResolverInterface
|
||||
{
|
||||
/**
|
||||
* @return RoleDefinition[]
|
||||
* @return iterable<RoleDefinition>
|
||||
*/
|
||||
public function determineRoles(InputInterface $input): array;
|
||||
public function determineRoles(InputInterface $input): iterable;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
@@ -25,8 +26,8 @@ class GenerateKeyCommand extends Command
|
||||
public const NAME = 'api-key:generate';
|
||||
|
||||
public function __construct(
|
||||
private ApiKeyServiceInterface $apiKeyService,
|
||||
private RoleResolverInterface $roleResolver,
|
||||
private readonly ApiKeyServiceInterface $apiKeyService,
|
||||
private readonly RoleResolverInterface $roleResolver,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -35,6 +36,8 @@ class GenerateKeyCommand extends Command
|
||||
{
|
||||
$authorOnly = Role::AUTHORED_SHORT_URLS->paramName();
|
||||
$domainOnly = Role::DOMAIN_SPECIFIC->paramName();
|
||||
$noOrphanVisits = Role::NO_ORPHAN_VISITS->paramName();
|
||||
|
||||
$help = <<<HELP
|
||||
The <info>%command.name%</info> generates a new valid API key.
|
||||
|
||||
@@ -52,12 +55,13 @@ class GenerateKeyCommand extends Command
|
||||
|
||||
* Can interact with short URLs created with this API key: <info>%command.full_name% --{$authorOnly}</info>
|
||||
* Can interact with short URLs for one domain: <info>%command.full_name% --{$domainOnly}=example.com</info>
|
||||
* Both: <info>%command.full_name% --{$authorOnly} --{$domainOnly}=example.com</info>
|
||||
* Cannot see orphan visits: <info>%command.full_name% --{$noOrphanVisits}</info>
|
||||
* All: <info>%command.full_name% --{$authorOnly} --{$domainOnly}=example.com --{$noOrphanVisits}</info>
|
||||
HELP;
|
||||
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Generates a new valid API key.')
|
||||
->setDescription('Generate a new valid API key.')
|
||||
->addOption(
|
||||
'name',
|
||||
'm',
|
||||
@@ -85,17 +89,24 @@ class GenerateKeyCommand extends Command
|
||||
Role::DOMAIN_SPECIFIC->value,
|
||||
),
|
||||
)
|
||||
->addOption(
|
||||
$noOrphanVisits,
|
||||
'o',
|
||||
InputOption::VALUE_NONE,
|
||||
sprintf('Adds the "%s" role to the new API key.', Role::NO_ORPHAN_VISITS->value),
|
||||
)
|
||||
->setHelp($help);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$expirationDate = $input->getOption('expiration-date');
|
||||
$apiKey = $this->apiKeyService->create(
|
||||
isset($expirationDate) ? Chronos::parse($expirationDate) : null,
|
||||
$input->getOption('name'),
|
||||
...$this->roleResolver->determineRoles($input),
|
||||
);
|
||||
|
||||
$apiKey = $this->apiKeyService->create(ApiKeyMeta::fromParams(
|
||||
name: $input->getOption('name'),
|
||||
expirationDate: isset($expirationDate) ? Chronos::parse($expirationDate) : null,
|
||||
roleDefinitions: $this->roleResolver->determineRoles($input),
|
||||
));
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->success(sprintf('Generated API key: "%s"', $apiKey->toString()));
|
||||
|
||||
43
module/CLI/src/Command/Api/InitialApiKeyCommand.php
Normal file
43
module/CLI/src/Command/Api/InitialApiKeyCommand.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class InitialApiKeyCommand extends Command
|
||||
{
|
||||
public const NAME = 'api-key:initial';
|
||||
|
||||
public function __construct(private readonly ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setHidden()
|
||||
->setName(self::NAME)
|
||||
->setDescription('Tries to create initial API key')
|
||||
->addArgument('apiKey', InputArgument::REQUIRED, 'The initial API to create');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$key = $input->getArgument('apiKey');
|
||||
$result = $this->apiKeyService->createInitial($key);
|
||||
|
||||
if ($result === null && $output->isVerbose()) {
|
||||
$output->writeln('<comment>Other API keys already exist. Initial API key creation skipped.</comment>');
|
||||
}
|
||||
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function array_filter;
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
@@ -27,7 +27,7 @@ class ListKeysCommand extends Command
|
||||
|
||||
public const NAME = 'api-key:list';
|
||||
|
||||
public function __construct(private ApiKeyServiceInterface $apiKeyService)
|
||||
public function __construct(private readonly ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -49,7 +49,7 @@ class ListKeysCommand extends Command
|
||||
{
|
||||
$enabledOnly = $input->getOption('enabled-only');
|
||||
|
||||
$rows = map($this->apiKeyService->listKeys($enabledOnly), function (ApiKey $apiKey) use ($enabledOnly) {
|
||||
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
|
||||
$expiration = $apiKey->getExpirationDate();
|
||||
$messagePattern = $this->determineMessagePattern($apiKey);
|
||||
|
||||
@@ -60,14 +60,11 @@ class ListKeysCommand extends Command
|
||||
}
|
||||
$rowData[] = $expiration?->toAtomString() ?? '-';
|
||||
$rowData[] = ApiKey::isAdmin($apiKey) ? 'Admin' : implode("\n", $apiKey->mapRoles(
|
||||
fn (Role $role, array $meta) =>
|
||||
empty($meta)
|
||||
? $role->toFriendlyName()
|
||||
: sprintf('%s: %s', $role->toFriendlyName(), Role::domainAuthorityFromMeta($meta)),
|
||||
fn (Role $role, array $meta) => $role->toFriendlyName($meta),
|
||||
));
|
||||
|
||||
return $rowData;
|
||||
});
|
||||
}, $this->apiKeyService->listKeys($enabledOnly));
|
||||
|
||||
ShlinkTable::withRowSeparators($output)->render(array_filter([
|
||||
'Key',
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\CLI\Command\Db;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
@@ -15,10 +14,11 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Lock\LockFactory;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
use Throwable;
|
||||
|
||||
use function Functional\contains;
|
||||
use function Functional\map;
|
||||
use function Functional\some;
|
||||
use function array_map;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\some;
|
||||
|
||||
class CreateDatabaseCommand extends AbstractDatabaseCommand
|
||||
{
|
||||
@@ -53,9 +53,7 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$this->checkDbExists();
|
||||
|
||||
if ($this->schemaExists()) {
|
||||
if ($this->databaseTablesExist()) {
|
||||
$io->success('Database already exists. Run "db:migrate" command to make sure it is up to date.');
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
@@ -68,35 +66,31 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function checkDbExists(): void
|
||||
private function databaseTablesExist(): bool
|
||||
{
|
||||
if ($this->regularConn->getDriver()->getDatabasePlatform() instanceof SqlitePlatform) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In order to create the new database, we have to use a connection where the dbname was not set.
|
||||
// Otherwise, it will fail to connect and will not be able to create the new database
|
||||
$schemaManager = $this->noDbNameConn->createSchemaManager();
|
||||
$databases = $schemaManager->listDatabases();
|
||||
// We cannot use getDatabase() to get the database name here, because then the driver will try to connect, and
|
||||
// it does not exist yet. We need to read from the raw params instead.
|
||||
$shlinkDatabase = $this->regularConn->getParams()['dbname'] ?? null;
|
||||
|
||||
if ($shlinkDatabase !== null && ! contains($databases, $shlinkDatabase)) {
|
||||
$schemaManager->createDatabase($shlinkDatabase);
|
||||
}
|
||||
}
|
||||
|
||||
private function schemaExists(): bool
|
||||
{
|
||||
$schemaManager = $this->regularConn->createSchemaManager();
|
||||
$existingTables = $schemaManager->listTableNames();
|
||||
|
||||
$existingTables = $this->ensureDatabaseExistsAndGetTables();
|
||||
$allMetadata = $this->em->getMetadataFactory()->getAllMetadata();
|
||||
$shlinkTables = map($allMetadata, static fn (ClassMetadata $metadata) => $metadata->getTableName());
|
||||
$shlinkTables = array_map(static fn (ClassMetadata $metadata) => $metadata->getTableName(), $allMetadata);
|
||||
|
||||
// If at least one of the shlink tables exist, we will consider the database exists somehow.
|
||||
// Any other inconsistency will be taken care of by the migrations.
|
||||
return some($shlinkTables, static fn (string $shlinkTable) => contains($existingTables, $shlinkTable));
|
||||
return some($shlinkTables, static fn (string $shlinkTable) => contains($shlinkTable, $existingTables));
|
||||
}
|
||||
|
||||
private function ensureDatabaseExistsAndGetTables(): array
|
||||
{
|
||||
try {
|
||||
// Trying to list tables requires opening a connection to configured database.
|
||||
// If it fails, it means it does not exist yet.
|
||||
return $this->regularConn->createSchemaManager()->listTableNames();
|
||||
} catch (Throwable) {
|
||||
// We cannot use getDatabase() to get the database name here, because then the driver will try to connect.
|
||||
// Instead, we read from the raw params.
|
||||
$shlinkDatabase = $this->regularConn->getParams()['dbname'] ?? '';
|
||||
// Create the database using a connection where the dbname was not set.
|
||||
$this->noDbNameConn->createSchemaManager()->createDatabase($shlinkDatabase);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function Functional\filter;
|
||||
use function Functional\invoke;
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
|
||||
@@ -23,7 +23,7 @@ class DomainRedirectsCommand extends Command
|
||||
{
|
||||
public const NAME = 'domain:redirects';
|
||||
|
||||
public function __construct(private DomainServiceInterface $domainService)
|
||||
public function __construct(private readonly DomainServiceInterface $domainService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -52,9 +52,9 @@ class DomainRedirectsCommand extends Command
|
||||
$askNewDomain = static fn () => $io->ask('Domain authority for which you want to set specific redirects');
|
||||
|
||||
/** @var string[] $availableDomains */
|
||||
$availableDomains = invoke(
|
||||
filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault),
|
||||
'toString',
|
||||
$availableDomains = array_map(
|
||||
static fn (DomainItem $item) => $item->toString(),
|
||||
array_filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault),
|
||||
);
|
||||
if (empty($availableDomains)) {
|
||||
$input->setArgument('domain', $askNewDomain());
|
||||
|
||||
@@ -14,13 +14,13 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
|
||||
class ListDomainsCommand extends Command
|
||||
{
|
||||
public const NAME = 'domain:list';
|
||||
|
||||
public function __construct(private DomainServiceInterface $domainService)
|
||||
public function __construct(private readonly DomainServiceInterface $domainService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -47,7 +47,7 @@ class ListDomainsCommand extends Command
|
||||
|
||||
$table->render(
|
||||
$showRedirects ? [...$commonFields, '"Not found" redirects'] : $commonFields,
|
||||
map($domains, function (DomainItem $domain) use ($showRedirects) {
|
||||
array_map(function (DomainItem $domain) use ($showRedirects) {
|
||||
$commonValues = [$domain->toString(), $domain->isDefault ? 'Yes' : 'No'];
|
||||
|
||||
return $showRedirects
|
||||
@@ -56,7 +56,7 @@ class ListDomainsCommand extends Command
|
||||
$this->notFoundRedirectsToString($domain->notFoundRedirectConfig),
|
||||
]
|
||||
: $commonValues;
|
||||
}),
|
||||
}, $domains),
|
||||
);
|
||||
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
|
||||
@@ -20,10 +20,9 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function array_map;
|
||||
use function array_unique;
|
||||
use function explode;
|
||||
use function Functional\curry;
|
||||
use function Functional\flatten;
|
||||
use function Functional\unique;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\flatten;
|
||||
use function sprintf;
|
||||
|
||||
class CreateShortUrlCommand extends Command
|
||||
@@ -144,8 +143,8 @@ class CreateShortUrlCommand extends Command
|
||||
return ExitCode::EXIT_FAILURE;
|
||||
}
|
||||
|
||||
$explodeWithComma = curry(explode(...))(',');
|
||||
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
||||
$explodeWithComma = static fn (string $tag) => explode(',', $tag);
|
||||
$tags = array_unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
||||
$customSlug = $input->getOption('custom-slug');
|
||||
$maxVisits = $input->getOption('max-visits');
|
||||
$shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength;
|
||||
|
||||
@@ -23,9 +23,9 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_pad;
|
||||
use function explode;
|
||||
use function Functional\map;
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
@@ -184,10 +184,10 @@ class ListShortUrlsCommand extends Command
|
||||
): Paginator {
|
||||
$shortUrls = $this->shortUrlService->listShortUrls($params);
|
||||
|
||||
$rows = map($shortUrls, function (ShortUrl $shortUrl) use ($columnsMap) {
|
||||
$rows = array_map(function (ShortUrl $shortUrl) use ($columnsMap) {
|
||||
$rawShortUrl = $this->transformer->transform($shortUrl);
|
||||
return map($columnsMap, fn (callable $call) => $call($rawShortUrl, $shortUrl));
|
||||
});
|
||||
return array_map(fn (callable $call) => $call($rawShortUrl, $shortUrl), $columnsMap);
|
||||
}, [...$shortUrls]);
|
||||
|
||||
ShlinkTable::default($output)->render(
|
||||
array_keys($columnsMap),
|
||||
|
||||
@@ -13,13 +13,13 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function Functional\map;
|
||||
use function array_map;
|
||||
|
||||
class ListTagsCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:list';
|
||||
|
||||
public function __construct(private TagServiceInterface $tagService)
|
||||
public function __construct(private readonly TagServiceInterface $tagService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -44,9 +44,9 @@ class ListTagsCommand extends Command
|
||||
return [['No tags found', '-', '-']];
|
||||
}
|
||||
|
||||
return map(
|
||||
$tags,
|
||||
return array_map(
|
||||
static fn (TagInfo $tagInfo) => [$tagInfo->tag, $tagInfo->shortUrlsCount, $tagInfo->visitsSummary->total],
|
||||
[...$tags],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function array_keys;
|
||||
use function Functional\map;
|
||||
use function Functional\select_keys;
|
||||
use function array_map;
|
||||
use function Shlinkio\Shlink\Common\buildDateRange;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\select_keys;
|
||||
use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly;
|
||||
|
||||
abstract class AbstractVisitsListCommand extends Command
|
||||
@@ -49,7 +49,7 @@ abstract class AbstractVisitsListCommand extends Command
|
||||
private function resolveRowsAndHeaders(Paginator $paginator): array
|
||||
{
|
||||
$extraKeys = [];
|
||||
$rows = map($paginator->getCurrentPageResults(), function (Visit $visit) use (&$extraKeys) {
|
||||
$rows = array_map(function (Visit $visit) use (&$extraKeys) {
|
||||
$extraFields = $this->mapExtraFields($visit);
|
||||
$extraKeys = array_keys($extraFields);
|
||||
|
||||
@@ -60,9 +60,10 @@ abstract class AbstractVisitsListCommand extends Command
|
||||
...$extraFields,
|
||||
];
|
||||
|
||||
// Filter out unknown keys
|
||||
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys]);
|
||||
});
|
||||
$extra = map($extraKeys, camelCaseToHumanFriendly(...));
|
||||
}, [...$paginator->getCurrentPageResults()]);
|
||||
$extra = array_map(camelCaseToHumanFriendly(...), $extraKeys);
|
||||
|
||||
return [
|
||||
$rows,
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
|
||||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
@@ -41,7 +42,7 @@ class DownloadGeoLiteDbCommand extends Command
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
try {
|
||||
$this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) use ($io): void {
|
||||
$result = $this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) use ($io): void {
|
||||
$io->text(sprintf('<fg=blue>%s GeoLite2 db file...</>', $olderDbExists ? 'Updating' : 'Downloading'));
|
||||
$this->progressBar = new ProgressBar($io);
|
||||
}, function (int $total, int $downloaded): void {
|
||||
@@ -49,6 +50,11 @@ class DownloadGeoLiteDbCommand extends Command
|
||||
$this->progressBar?->setProgress($downloaded);
|
||||
});
|
||||
|
||||
if ($result === GeolocationResult::LICENSE_MISSING) {
|
||||
$io->warning('It was not possible to download GeoLite2 db, because a license was not provided.');
|
||||
return ExitCode::EXIT_WARNING;
|
||||
}
|
||||
|
||||
if ($this->progressBar === null) {
|
||||
$io->info('GeoLite2 db file is up to date.');
|
||||
} else {
|
||||
@@ -58,21 +64,26 @@ class DownloadGeoLiteDbCommand extends Command
|
||||
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
} catch (GeolocationDbUpdateFailedException $e) {
|
||||
$olderDbExists = $e->olderDbExists();
|
||||
|
||||
if ($olderDbExists) {
|
||||
$io->warning(
|
||||
'GeoLite2 db file update failed. Visits will continue to be located with the old version.',
|
||||
);
|
||||
} else {
|
||||
$io->error('GeoLite2 db file download failed. It will not be possible to locate visits.');
|
||||
}
|
||||
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()?->renderThrowable($e, $io);
|
||||
}
|
||||
|
||||
return $olderDbExists ? ExitCode::EXIT_WARNING : ExitCode::EXIT_FAILURE;
|
||||
return $this->processGeoLiteUpdateError($e, $io);
|
||||
}
|
||||
}
|
||||
|
||||
private function processGeoLiteUpdateError(GeolocationDbUpdateFailedException $e, SymfonyStyle $io): int
|
||||
{
|
||||
$olderDbExists = $e->olderDbExists();
|
||||
|
||||
if ($olderDbExists) {
|
||||
$io->warning(
|
||||
'GeoLite2 db file update failed. Visits will continue to be located with the old version.',
|
||||
);
|
||||
} else {
|
||||
$io->error('GeoLite2 db file download failed. It will not be possible to locate visits.');
|
||||
}
|
||||
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()?->renderThrowable($e, $io);
|
||||
}
|
||||
|
||||
return $olderDbExists ? ExitCode::EXIT_WARNING : ExitCode::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
||||
$buildTimestamp = $this->resolveBuildTimestamp($meta);
|
||||
$buildDate = Chronos::createFromTimestamp($buildTimestamp);
|
||||
|
||||
return Chronos::now()->gt($buildDate->addDays(35));
|
||||
return Chronos::now()->greaterThan($buildDate->addDays(35));
|
||||
}
|
||||
|
||||
private function resolveBuildTimestamp(Metadata $meta): int
|
||||
@@ -108,8 +108,7 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
||||
$this->dbUpdater->downloadFreshCopy($this->wrapHandleProgressCallback($handleProgress, $olderDbExists));
|
||||
return $olderDbExists ? GeolocationResult::DB_UPDATED : GeolocationResult::DB_CREATED;
|
||||
} catch (MissingLicenseException) {
|
||||
// If there's no license key, just ignore the error
|
||||
return GeolocationResult::CHECK_SKIPPED;
|
||||
return GeolocationResult::LICENSE_MISSING;
|
||||
} catch (DbUpdateException | WrongIpException $e) {
|
||||
throw $olderDbExists
|
||||
? GeolocationDbUpdateFailedException::withOlderDb($e)
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Shlinkio\Shlink\CLI\GeoLite;
|
||||
enum GeolocationResult
|
||||
{
|
||||
case CHECK_SKIPPED;
|
||||
case LICENSE_MISSING;
|
||||
case DB_CREATED;
|
||||
case DB_UPDATED;
|
||||
case DB_IS_UP_TO_DATE;
|
||||
|
||||
@@ -23,8 +23,8 @@ class ProcessRunner implements ProcessRunnerInterface
|
||||
public function __construct(private ProcessHelper $helper, ?callable $createProcess = null)
|
||||
{
|
||||
$this->createProcess = $createProcess !== null
|
||||
? Closure::fromCallable($createProcess)
|
||||
: static fn (array $cmd) => new Process($cmd, null, null, null, LockedCommandConfig::DEFAULT_TTL);
|
||||
? $createProcess(...)
|
||||
: static fn (array $cmd) => new Process($cmd, timeout: LockedCommandConfig::DEFAULT_TTL);
|
||||
}
|
||||
|
||||
public function run(OutputInterface $output, array $cmd): void
|
||||
|
||||
@@ -8,30 +8,30 @@ use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Helper\TableSeparator;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function Functional\intersperse;
|
||||
use function array_pop;
|
||||
|
||||
final class ShlinkTable
|
||||
{
|
||||
private const DEFAULT_STYLE_NAME = 'default';
|
||||
private const TABLE_TITLE_STYLE = '<options=bold> %s </>';
|
||||
|
||||
private function __construct(private readonly Table $baseTable, private readonly bool $withRowSeparators)
|
||||
private function __construct(private readonly Table $baseTable, private readonly bool $withRowSeparators = false)
|
||||
{
|
||||
}
|
||||
|
||||
public static function default(OutputInterface $output): self
|
||||
{
|
||||
return new self(new Table($output), false);
|
||||
return new self(new Table($output));
|
||||
}
|
||||
|
||||
public static function withRowSeparators(OutputInterface $output): self
|
||||
{
|
||||
return new self(new Table($output), true);
|
||||
return new self(new Table($output), withRowSeparators: true);
|
||||
}
|
||||
|
||||
public static function fromBaseTable(Table $baseTable): self
|
||||
{
|
||||
return new self($baseTable, false);
|
||||
return new self($baseTable);
|
||||
}
|
||||
|
||||
public function render(array $headers, array $rows, ?string $footerTitle = null, ?string $headerTitle = null): void
|
||||
@@ -39,7 +39,7 @@ final class ShlinkTable
|
||||
$style = Table::getStyleDefinition(self::DEFAULT_STYLE_NAME);
|
||||
$style->setFooterTitleFormat(self::TABLE_TITLE_STYLE)
|
||||
->setHeaderTitleFormat(self::TABLE_TITLE_STYLE);
|
||||
$tableRows = $this->withRowSeparators ? intersperse($rows, new TableSeparator()) : $rows;
|
||||
$tableRows = $this->withRowSeparators ? $this->addRowSeparators($rows) : $rows;
|
||||
|
||||
$table = clone $this->baseTable;
|
||||
$table->setStyle($style)
|
||||
@@ -49,4 +49,20 @@ final class ShlinkTable
|
||||
->setHeaderTitle($headerTitle)
|
||||
->render();
|
||||
}
|
||||
|
||||
private function addRowSeparators(array $rows): array
|
||||
{
|
||||
$aggregation = [];
|
||||
$separator = new TableSeparator();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$aggregation[] = $row;
|
||||
$aggregation[] = $separator;
|
||||
}
|
||||
|
||||
// Remove last separator
|
||||
array_pop($aggregation);
|
||||
|
||||
return $aggregation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,7 @@ use function unlink;
|
||||
|
||||
class ImportShortUrlsTest extends CliTestCase
|
||||
{
|
||||
/**
|
||||
* @var false|string|null
|
||||
* @todo Use native type once PHP 8.1 support is dropped
|
||||
*/
|
||||
private mixed $tempCsvFile = null;
|
||||
private false|string|null $tempCsvFile = null;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
|
||||
26
module/CLI/test-cli/Command/InitialApiKeyTest.php
Normal file
26
module/CLI/test-cli/Command/InitialApiKeyTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioCliTest\Shlink\CLI\Command;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\InitialApiKeyCommand;
|
||||
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
||||
|
||||
class InitialApiKeyTest extends CliTestCase
|
||||
{
|
||||
#[Test]
|
||||
public function createsNoKeyWhenOtherApiKeysAlreadyExist(): void
|
||||
{
|
||||
[$output] = $this->exec([InitialApiKeyCommand::NAME, 'new_api_key', '-v']);
|
||||
|
||||
self::assertEquals(
|
||||
<<<OUT
|
||||
Other API keys already exist. Initial API key creation skipped.
|
||||
|
||||
OUT,
|
||||
$output,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,36 +24,40 @@ class ListApiKeysTest extends CliTestCase
|
||||
|
||||
public static function provideFlags(): iterable
|
||||
{
|
||||
$expiredApiKeyDate = Chronos::now()->subDay()->startOfDay()->toAtomString();
|
||||
$expiredApiKeyDate = Chronos::now()->subDays(1)->startOfDay()->toAtomString();
|
||||
$enabledOnlyOutput = <<<OUT
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| Key | Name | Expiration date | Roles |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| valid_api_key | - | - | Admin |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| expired_api_key | - | {$expiredApiKeyDate} | Admin |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| author_api_key | - | - | Author only |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| domain_api_key | - | - | Domain only: example.com |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
| Key | Name | Expiration date | Roles |
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
| valid_api_key | - | - | Admin |
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
| expired_api_key | - | {$expiredApiKeyDate} | Admin |
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
| author_api_key | - | - | Author only |
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
| domain_api_key | - | - | Domain only: example.com |
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
| no_orphans_api_key | - | - | No orphan visits |
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
|
||||
OUT;
|
||||
|
||||
yield 'no flags' => [[], <<<OUT
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| Key | Name | Is enabled | Expiration date | Roles |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| valid_api_key | - | +++ | - | Admin |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| disabled_api_key | - | --- | - | Admin |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| expired_api_key | - | --- | {$expiredApiKeyDate} | Admin |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| author_api_key | - | +++ | - | Author only |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| domain_api_key | - | +++ | - | Domain only: example.com |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| Key | Name | Is enabled | Expiration date | Roles |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| valid_api_key | - | +++ | - | Admin |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| disabled_api_key | - | --- | - | Admin |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| expired_api_key | - | --- | {$expiredApiKeyDate} | Admin |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| author_api_key | - | +++ | - | Author only |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| domain_api_key | - | +++ | - | Domain only: example.com |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| no_orphans_api_key | - | +++ | - | No orphan visits |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
|
||||
OUT];
|
||||
yield '-e' => [['-e'], $enabledOnlyOutput];
|
||||
|
||||
@@ -16,8 +16,6 @@ use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
use function Functional\map;
|
||||
|
||||
class RoleResolverTest extends TestCase
|
||||
{
|
||||
private RoleResolver $resolver;
|
||||
@@ -40,7 +38,7 @@ class RoleResolverTest extends TestCase
|
||||
'example.com',
|
||||
)->willReturn(self::domainWithId(Domain::withAuthority('example.com')));
|
||||
|
||||
$result = $this->resolver->determineRoles($input);
|
||||
$result = [...$this->resolver->determineRoles($input)];
|
||||
|
||||
self::assertEquals($expectedRoles, $result);
|
||||
}
|
||||
@@ -49,10 +47,13 @@ class RoleResolverTest extends TestCase
|
||||
{
|
||||
$domain = self::domainWithId(Domain::withAuthority('example.com'));
|
||||
$buildInput = static fn (array $definition) => function (TestCase $test) use ($definition): InputInterface {
|
||||
$returnMap = [];
|
||||
foreach ($definition as $param => $returnValue) {
|
||||
$returnMap[] = [$param, $returnValue];
|
||||
}
|
||||
|
||||
$input = $test->createStub(InputInterface::class);
|
||||
$input->method('getOption')->willReturnMap(
|
||||
map($definition, static fn (mixed $returnValue, string $param) => [$param, $returnValue]),
|
||||
);
|
||||
$input->method('getOption')->willReturnMap($returnMap);
|
||||
|
||||
return $input;
|
||||
};
|
||||
@@ -111,7 +112,7 @@ class RoleResolverTest extends TestCase
|
||||
|
||||
$this->expectException(InvalidRoleConfigException::class);
|
||||
|
||||
$this->resolver->determineRoles($input);
|
||||
[...$this->resolver->determineRoles($input)];
|
||||
}
|
||||
|
||||
private static function domainWithId(Domain $domain): Domain
|
||||
|
||||
@@ -10,20 +10,18 @@ use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class DisableKeyCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->createMock(ApiKeyServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DisableKeyCommand($this->apiKeyService));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DisableKeyCommand($this->apiKeyService));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@@ -10,16 +10,15 @@ use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class GenerateKeyCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
@@ -30,15 +29,14 @@ class GenerateKeyCommandTest extends TestCase
|
||||
$roleResolver->method('determineRoles')->with($this->isInstanceOf(InputInterface::class))->willReturn([]);
|
||||
|
||||
$command = new GenerateKeyCommand($this->apiKeyService, $roleResolver);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function noExpirationDateIsDefinedIfNotProvided(): void
|
||||
{
|
||||
$this->apiKeyService->expects($this->once())->method('create')->with(
|
||||
$this->isNull(),
|
||||
$this->isNull(),
|
||||
$this->callback(fn (ApiKeyMeta $meta) => $meta->name === null && $meta->expirationDate === null),
|
||||
)->willReturn(ApiKey::create());
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
@@ -51,8 +49,7 @@ class GenerateKeyCommandTest extends TestCase
|
||||
public function expirationDateIsDefinedIfProvided(): void
|
||||
{
|
||||
$this->apiKeyService->expects($this->once())->method('create')->with(
|
||||
$this->isInstanceOf(Chronos::class),
|
||||
$this->isNull(),
|
||||
$this->callback(fn (ApiKeyMeta $meta) => $meta->expirationDate instanceof Chronos),
|
||||
)->willReturn(ApiKey::create());
|
||||
|
||||
$this->commandTester->execute([
|
||||
@@ -64,8 +61,7 @@ class GenerateKeyCommandTest extends TestCase
|
||||
public function nameIsDefinedIfProvided(): void
|
||||
{
|
||||
$this->apiKeyService->expects($this->once())->method('create')->with(
|
||||
$this->isNull(),
|
||||
$this->isType('string'),
|
||||
$this->callback(fn (ApiKeyMeta $meta) => $meta->name === 'Alice'),
|
||||
)->willReturn(ApiKey::create());
|
||||
|
||||
$this->commandTester->execute([
|
||||
|
||||
54
module/CLI/test/Command/Api/InitialApiKeyCommandTest.php
Normal file
54
module/CLI/test/Command/Api/InitialApiKeyCommandTest.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\InitialApiKeyCommand;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class InitialApiKeyCommandTest extends TestCase
|
||||
{
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->createMock(ApiKeyServiceInterface::class);
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new InitialApiKeyCommand($this->apiKeyService));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideParams')]
|
||||
public function initialKeyIsCreatedWithProvidedValue(?ApiKey $result, bool $verbose, string $expectedOutput): void
|
||||
{
|
||||
$this->apiKeyService->expects($this->once())->method('createInitial')->with('the_key')->willReturn($result);
|
||||
|
||||
$this->commandTester->execute(
|
||||
['apiKey' => 'the_key'],
|
||||
['verbosity' => $verbose ? OutputInterface::VERBOSITY_VERBOSE : OutputInterface::VERBOSITY_NORMAL],
|
||||
);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertEquals($expectedOutput, $output);
|
||||
}
|
||||
|
||||
public static function provideParams(): iterable
|
||||
{
|
||||
yield 'api key created, no verbose' => [ApiKey::create(), false, ''];
|
||||
yield 'api key created, verbose' => [ApiKey::create(), true, ''];
|
||||
yield 'no api key created, no verbose' => [null, false, ''];
|
||||
yield 'no api key created, verbose' => [null, true, <<<OUT
|
||||
Other API keys already exist. Initial API key creation skipped.
|
||||
|
||||
OUT,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -15,20 +15,18 @@ use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class ListKeysCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->createMock(ApiKeyServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new ListKeysCommand($this->apiKeyService));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new ListKeysCommand($this->apiKeyService));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideKeysAndOutputs')]
|
||||
@@ -49,7 +47,7 @@ class ListKeysCommandTest extends TestCase
|
||||
yield 'all keys' => [
|
||||
[
|
||||
$apiKey1 = ApiKey::create()->disable(),
|
||||
$apiKey2 = ApiKey::fromMeta(ApiKeyMeta::withExpirationDate($dateInThePast)),
|
||||
$apiKey2 = ApiKey::fromMeta(ApiKeyMeta::fromParams(expirationDate: $dateInThePast)),
|
||||
$apiKey3 = ApiKey::create(),
|
||||
],
|
||||
false,
|
||||
@@ -117,9 +115,9 @@ class ListKeysCommandTest extends TestCase
|
||||
];
|
||||
yield 'with names' => [
|
||||
[
|
||||
$apiKey1 = ApiKey::fromMeta(ApiKeyMeta::withName('Alice')),
|
||||
$apiKey2 = ApiKey::fromMeta(ApiKeyMeta::withName('Alice and Bob')),
|
||||
$apiKey3 = ApiKey::fromMeta(ApiKeyMeta::withName('')),
|
||||
$apiKey1 = ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'Alice')),
|
||||
$apiKey2 = ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'Alice and Bob')),
|
||||
$apiKey3 = ApiKey::fromMeta(ApiKeyMeta::fromParams(name: '')),
|
||||
$apiKey4 = ApiKey::create(),
|
||||
],
|
||||
true,
|
||||
|
||||
@@ -7,18 +7,18 @@ namespace ShlinkioTest\Shlink\CLI\Command\Db;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\Persistence\Mapping\ClassMetadataFactory;
|
||||
use Exception;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Db\CreateDatabaseCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Lock\LockFactory;
|
||||
@@ -27,8 +27,6 @@ use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
class CreateDatabaseCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ProcessRunnerInterface $processHelper;
|
||||
private MockObject & Connection $regularConn;
|
||||
@@ -63,23 +61,20 @@ class CreateDatabaseCommandTest extends TestCase
|
||||
$noDbNameConn->method('createSchemaManager')->withAnyParameters()->willReturn($this->schemaManager);
|
||||
|
||||
$command = new CreateDatabaseCommand($locker, $this->processHelper, $phpExecutableFinder, $em, $noDbNameConn);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function successMessageIsPrintedIfDatabaseAlreadyExists(): void
|
||||
{
|
||||
$shlinkDatabase = 'shlink_database';
|
||||
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
|
||||
$this->regularConn->expects($this->never())->method('getParams');
|
||||
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
|
||||
|
||||
$metadataMock = $this->createMock(ClassMetadata::class);
|
||||
$metadataMock->expects($this->once())->method('getTableName')->willReturn('foo_table');
|
||||
$this->metadataFactory->method('getAllMetadata')->willReturn([$metadataMock]);
|
||||
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(
|
||||
['foo', $shlinkDatabase, 'bar'],
|
||||
);
|
||||
$this->schemaManager->expects($this->never())->method('createDatabase');
|
||||
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn(['foo_table', 'bar_table']);
|
||||
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
@@ -90,15 +85,13 @@ class CreateDatabaseCommandTest extends TestCase
|
||||
#[Test]
|
||||
public function databaseIsCreatedIfItDoesNotExist(): void
|
||||
{
|
||||
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
|
||||
|
||||
$shlinkDatabase = 'shlink_database';
|
||||
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
|
||||
$this->metadataFactory->method('getAllMetadata')->willReturn([]);
|
||||
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(['foo', 'bar']);
|
||||
$this->schemaManager->expects($this->once())->method('createDatabase')->with($shlinkDatabase);
|
||||
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn(
|
||||
['foo_table', 'bar_table'],
|
||||
);
|
||||
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
|
||||
$this->schemaManager->expects($this->once())->method('listTableNames')->willThrowException(new Exception(''));
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
}
|
||||
@@ -106,14 +99,12 @@ class CreateDatabaseCommandTest extends TestCase
|
||||
#[Test, DataProvider('provideEmptyDatabase')]
|
||||
public function tablesAreCreatedIfDatabaseIsEmpty(array $tables): void
|
||||
{
|
||||
$shlinkDatabase = 'shlink_database';
|
||||
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
|
||||
$this->regularConn->expects($this->never())->method('getParams');
|
||||
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
|
||||
|
||||
$metadata = $this->createMock(ClassMetadata::class);
|
||||
$metadata->method('getTableName')->willReturn('shlink_table');
|
||||
$this->metadataFactory->method('getAllMetadata')->willReturn([$metadata]);
|
||||
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(
|
||||
['foo', $shlinkDatabase, 'bar'],
|
||||
);
|
||||
$this->schemaManager->expects($this->never())->method('createDatabase');
|
||||
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn($tables);
|
||||
$this->processHelper->expects($this->once())->method('run')->with($this->isInstanceOf(OutputInterface::class), [
|
||||
@@ -122,7 +113,6 @@ class CreateDatabaseCommandTest extends TestCase
|
||||
CreateDatabaseCommand::DOCTRINE_CREATE_SCHEMA_COMMAND,
|
||||
'--no-interaction',
|
||||
]);
|
||||
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
@@ -136,18 +126,4 @@ class CreateDatabaseCommandTest extends TestCase
|
||||
yield 'no tables' => [[]];
|
||||
yield 'migrations table' => [['non_shlink_table']];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function databaseCheckIsSkippedForSqlite(): void
|
||||
{
|
||||
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(SqlitePlatform::class));
|
||||
|
||||
$this->regularConn->expects($this->never())->method('getParams');
|
||||
$this->metadataFactory->expects($this->once())->method('getAllMetadata')->willReturn([]);
|
||||
$this->schemaManager->expects($this->never())->method('listDatabases');
|
||||
$this->schemaManager->expects($this->never())->method('createDatabase');
|
||||
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn(['foo_table', 'bar_table']);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Db\MigrateDatabaseCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Lock\LockFactory;
|
||||
@@ -18,8 +18,6 @@ use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
class MigrateDatabaseCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ProcessRunnerInterface $processHelper;
|
||||
|
||||
@@ -36,7 +34,7 @@ class MigrateDatabaseCommandTest extends TestCase
|
||||
$this->processHelper = $this->createMock(ProcessRunnerInterface::class);
|
||||
|
||||
$command = new MigrateDatabaseCommand($locker, $this->processHelper, $phpExecutableFinder);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@@ -14,22 +14,20 @@ use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function substr_count;
|
||||
|
||||
class DomainRedirectsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & DomainServiceInterface $domainService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->domainService = $this->createMock(DomainServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DomainRedirectsCommand($this->domainService));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DomainRedirectsCommand($this->domainService));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideDomains')]
|
||||
|
||||
@@ -17,13 +17,11 @@ use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class GetDomainVisitsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & VisitsStatsHelperInterface $visitsHelper;
|
||||
private MockObject & ShortUrlStringifierInterface $stringifier;
|
||||
@@ -33,7 +31,7 @@ class GetDomainVisitsCommandTest extends TestCase
|
||||
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
|
||||
$this->stringifier = $this->createMock(ShortUrlStringifierInterface::class);
|
||||
|
||||
$this->commandTester = $this->testerForCommand(
|
||||
$this->commandTester = CliTestUtils::testerForCommand(
|
||||
new GetDomainVisitsCommand($this->visitsHelper, $this->stringifier),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,20 +15,18 @@ use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class ListDomainsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & DomainServiceInterface $domainService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->domainService = $this->createMock(DomainServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new ListDomainsCommand($this->domainService));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new ListDomainsCommand($this->domainService));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideInputsAndOutputs')]
|
||||
|
||||
@@ -20,14 +20,12 @@ use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\UrlShorteningResult;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\UrlShortenerInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class CreateShortUrlCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & UrlShortenerInterface $urlShortener;
|
||||
private MockObject & ShortUrlStringifierInterface $stringifier;
|
||||
@@ -45,7 +43,7 @@ class CreateShortUrlCommandTest extends TestCase
|
||||
defaultShortCodesLength: 5,
|
||||
),
|
||||
);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@@ -12,7 +12,7 @@ use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function sprintf;
|
||||
@@ -21,15 +21,13 @@ use const PHP_EOL;
|
||||
|
||||
class DeleteShortUrlCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & DeleteShortUrlServiceInterface $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->service = $this->createMock(DeleteShortUrlServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DeleteShortUrlCommand($this->service));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DeleteShortUrlCommand($this->service));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@@ -13,20 +13,18 @@ use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\BulkDeleteResult;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class DeleteShortUrlVisitsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ShortUrlVisitsDeleterInterface $deleter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->deleter = $this->createMock(ShortUrlVisitsDeleterInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DeleteShortUrlVisitsCommand($this->deleter));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DeleteShortUrlVisitsCommand($this->deleter));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideCancellingInputs')]
|
||||
|
||||
@@ -20,7 +20,7 @@ use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function Shlinkio\Shlink\Common\buildDateRange;
|
||||
@@ -28,8 +28,6 @@ use function sprintf;
|
||||
|
||||
class GetShortUrlVisitsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & VisitsStatsHelperInterface $visitsHelper;
|
||||
|
||||
@@ -37,7 +35,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
|
||||
{
|
||||
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
|
||||
$command = new GetShortUrlVisitsCommand($this->visitsHelper);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@@ -21,7 +21,7 @@ use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function count;
|
||||
@@ -29,8 +29,6 @@ use function explode;
|
||||
|
||||
class ListShortUrlsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ShortUrlListServiceInterface $shortUrlService;
|
||||
|
||||
@@ -40,7 +38,7 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
$command = new ListShortUrlsCommand($this->shortUrlService, new ShortUrlDataTransformer(
|
||||
new ShortUrlStringifier([]),
|
||||
));
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -138,7 +136,7 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
|
||||
public static function provideOptionalFlags(): iterable
|
||||
{
|
||||
$apiKey = ApiKey::fromMeta(ApiKeyMeta::withName('my api key'));
|
||||
$apiKey = ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'my api key'));
|
||||
$key = $apiKey->toString();
|
||||
|
||||
yield 'tags only' => [
|
||||
|
||||
@@ -12,7 +12,7 @@ use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function sprintf;
|
||||
@@ -21,15 +21,13 @@ use const PHP_EOL;
|
||||
|
||||
class ResolveUrlCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ShortUrlResolverInterface $urlResolver;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->urlResolver = $this->createMock(ShortUrlResolverInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new ResolveUrlCommand($this->urlResolver));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new ResolveUrlCommand($this->urlResolver));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@@ -9,20 +9,18 @@ use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class DeleteTagsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & TagServiceInterface $tagService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tagService = $this->createMock(TagServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DeleteTagsCommand($this->tagService));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DeleteTagsCommand($this->tagService));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user